From 4e94e13e99c237b2c35a74b54e4b9d5dfc17496c Mon Sep 17 00:00:00 2001 From: Emre Date: Sun, 25 Sep 2022 19:24:21 +0300 Subject: [PATCH 01/15] add security tab --- .../Account/__snapshots__/index.jsx.snap | 696 ++++++++++++++++++ config/default.js | 1 + config/production.js | 1 + package-lock.json | 166 +++-- package.json | 2 + .../images/account/security/apple-store.svg | 34 + .../images/account/security/dicelogo.png | Bin 0 -> 3067 bytes .../images/account/security/dicelogobig.png | Bin 0 -> 23747 bytes .../images/account/security/dicelogosmall.png | Bin 0 -> 6681 bytes .../images/account/security/google-play.svg | 46 ++ .../images/account/security/green-close.svg | 3 + src/assets/images/account/security/mfa.svg | 4 + .../images/account/security/unsuccessful.svg | 3 + src/shared/actions/mfa.js | 90 +++ .../Settings/Account/MyAccount/styles.scss | 2 +- .../Settings/Account/Security/Modal/index.jsx | 78 ++ .../Account/Security/Modal/style.scss | 143 ++++ .../Settings/Account/Security/index.jsx | 388 ++++++++++ .../Settings/Account/Security/styles.scss | 226 ++++++ .../components/Settings/Account/index.jsx | 6 +- src/shared/containers/Settings.jsx | 28 +- src/shared/reducers/index.js | 2 + src/shared/reducers/mfa.js | 142 ++++ src/shared/services/mfa.js | 93 +++ 24 files changed, 2099 insertions(+), 55 deletions(-) create mode 100644 src/assets/images/account/security/apple-store.svg create mode 100644 src/assets/images/account/security/dicelogo.png create mode 100644 src/assets/images/account/security/dicelogobig.png create mode 100644 src/assets/images/account/security/dicelogosmall.png create mode 100644 src/assets/images/account/security/google-play.svg create mode 100644 src/assets/images/account/security/green-close.svg create mode 100644 src/assets/images/account/security/mfa.svg create mode 100644 src/assets/images/account/security/unsuccessful.svg create mode 100644 src/shared/actions/mfa.js create mode 100644 src/shared/components/Settings/Account/Security/Modal/index.jsx create mode 100644 src/shared/components/Settings/Account/Security/Modal/style.scss create mode 100644 src/shared/components/Settings/Account/Security/index.jsx create mode 100644 src/shared/components/Settings/Account/Security/styles.scss create mode 100644 src/shared/reducers/mfa.js create mode 100644 src/shared/services/mfa.js diff --git a/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap b/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap index 608ab744be..3b2db9879e 100644 --- a/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Settings/Account/__snapshots__/index.jsx.snap @@ -708,6 +708,702 @@ exports[`renders account setting page correctly 1`] = ` updatePassword={[Function]} updateProfile={[Function]} /> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/account/security/dicelogo.png b/src/assets/images/account/security/dicelogo.png new file mode 100644 index 0000000000000000000000000000000000000000..723b63b2e6750ae6584a86cb94ba3af2661f00da GIT binary patch literal 3067 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

Pt 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 ZmE{2L6bCHqzs&JBa?;A5swGW={|Azy8DRhb literal 0 HcmV?d00001 diff --git a/src/assets/images/account/security/dicelogosmall.png b/src/assets/images/account/security/dicelogosmall.png new file mode 100644 index 0000000000000000000000000000000000000000..c2be4e4a804a6d5327d68c4ba816e25f5a1c469c GIT binary patch literal 6681 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/account/security/google-play.svg b/src/assets/images/account/security/google-play.svg new file mode 100644 index 0000000000..29c2b6f805 --- /dev/null +++ b/src/assets/images/account/security/google-play.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/account/security/green-close.svg b/src/assets/images/account/security/green-close.svg new file mode 100644 index 0000000000..d3ff15c89d --- /dev/null +++ b/src/assets/images/account/security/green-close.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/account/security/mfa.svg b/src/assets/images/account/security/mfa.svg new file mode 100644 index 0000000000..33b5ab0cc7 --- /dev/null +++ b/src/assets/images/account/security/mfa.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/account/security/unsuccessful.svg b/src/assets/images/account/security/unsuccessful.svg new file mode 100644 index 0000000000..29ae859bd8 --- /dev/null +++ b/src/assets/images/account/security/unsuccessful.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/shared/actions/mfa.js b/src/shared/actions/mfa.js new file mode 100644 index 0000000000..6551e28ec9 --- /dev/null +++ b/src/shared/actions/mfa.js @@ -0,0 +1,90 @@ +import { createActions } from 'redux-actions'; +import { getService } from '../services/mfa'; + +/** + * @static + * @desc Creates an action that get user 2fa settings + * @param {String} userId Operation User id. + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function getUser2faDone(userId, tokenV3) { + return getService(tokenV3).getUser2fa(userId); +} + +/** + * @static + * @desc Creates an action that signals beginning of updating user 2fa settings. + * @return {Action} + */ +function updateUser2faInit() { } + +/** + * @static + * @desc Update user 2fa settings + * @param {Number} userId User id. + * @param {Boolean} mfaEnabled 2fa flag. + * @return {Action} + */ +async function updateUser2faDone(userId, mfaEnabled, tokenV3) { + return getService(tokenV3).updateUser2fa(userId, mfaEnabled); +} + +/** + * @static + * @desc Update user dice settings + * @param {Number} userId User id. + * @param {Boolean} diceEnabled dice flag. + * @return {Action} + */ +async function updateUserDiceDone(userId, diceEnabled, tokenV3) { + return getService(tokenV3).updateUserDice(userId, diceEnabled); +} + +/** + * @static + * @desc Creates an action that signals beginning of getting new dice connection + * @return {Action} + */ +function getNewDiceConnectionInit() { } + +/** + * @static + * @desc Get new Dice connection + * @param {Number} userId User id. + * @return {Action} + */ +async function getNewDiceConnectionDone(userId, tokenV3) { + return getService(tokenV3).getNewDiceConnection(userId); +} + +/** + * @static + * @desc Creates an action that signals beginning of getting dice connection + * @return {Action} + */ +function getDiceConnectionInit() { } + +/** + * @static + * @desc Get Dice connection + * @param {Number} userId User id. + * @param {Number} connectionId User id. + * @return {Action} + */ +async function getDiceConnectionDone(userId, connectionId, tokenV3) { + return getService(tokenV3).getDiceConnection(userId, connectionId); +} + +export default createActions({ + USERMFA: { + GET_USER2FA_DONE: getUser2faDone, + UPDATE_USER2FA_INIT: updateUser2faInit, + UPDATE_USER2FA_DONE: updateUser2faDone, + UPDATE_USER_DICE_DONE: updateUserDiceDone, + GET_NEW_DICE_CONNECTION_INIT: getNewDiceConnectionInit, + GET_NEW_DICE_CONNECTION_DONE: getNewDiceConnectionDone, + GET_DICE_CONNECTION_INIT: getDiceConnectionInit, + GET_DICE_CONNECTION_DONE: getDiceConnectionDone, + }, +}); diff --git a/src/shared/components/Settings/Account/MyAccount/styles.scss b/src/shared/components/Settings/Account/MyAccount/styles.scss index cc6afae006..ba61547185 100644 --- a/src/shared/components/Settings/Account/MyAccount/styles.scss +++ b/src/shared/components/Settings/Account/MyAccount/styles.scss @@ -349,7 +349,7 @@ padding: $pad-xxxxl; background-color: $color-tc-white; border-radius: 4px; - margin: $margin-sm 0 0; + margin: $margin-sm 0 $margin-xxxxl 0; .password-form { display: flex; diff --git a/src/shared/components/Settings/Account/Security/Modal/index.jsx b/src/shared/components/Settings/Account/Security/Modal/index.jsx new file mode 100644 index 0000000000..c9324d2f3c --- /dev/null +++ b/src/shared/components/Settings/Account/Security/Modal/index.jsx @@ -0,0 +1,78 @@ +import React, { useEffect } from 'react'; +import PT from 'prop-types'; +import DiceImage from 'assets/images/account/security/dicelogosmall.png'; +import CloseButton from 'assets/images/account/security/green-close.svg'; + +import './style.scss'; + +export default function DiceModal({ + children, onCancel, leftButtonName, leftButtonDisabled, leftButtonClick, + rightButtonName, rightButtonDisabled, rightButtonClick, +}) { + useEffect(() => { + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = 'unset'; }; + }, []); + + return ( + ( + +

event.stopPropagation()} + > +
+
+ diceid +
+
DICE ID authenticator setup
+ +
+
+
+ {children} +
+ +
+
+
{leftButtonName} +
+
{rightButtonName} +
+
+
+
+ + ) + ); +} +DiceModal.defaultProps = { + children: null, + leftButtonDisabled: false, + rightButtonDisabled: false, + +}; +DiceModal.propTypes = { + children: PT.node, + onCancel: PT.func.isRequired, + leftButtonName: PT.string.isRequired, + leftButtonDisabled: PT.bool, + leftButtonClick: PT.func.isRequired, + rightButtonName: PT.string.isRequired, + rightButtonDisabled: PT.bool, + rightButtonClick: PT.func.isRequired, +}; diff --git a/src/shared/components/Settings/Account/Security/Modal/style.scss b/src/shared/components/Settings/Account/Security/Modal/style.scss new file mode 100644 index 0000000000..2ffd61c795 --- /dev/null +++ b/src/shared/components/Settings/Account/Security/Modal/style.scss @@ -0,0 +1,143 @@ +@import "../../../style"; +@import "~styles/mixins"; + +.overlay { + background: #0c0c0c; + border: none; + height: 100%; + left: 0; + opacity: 0.85; + outline: none; + position: fixed; + top: 0; + width: 100%; + z-index: 950; +} + +.container { + background: #fff; + position: fixed; + width: 1000px; + height: 752px; + top: 50%; + left: 50%; + padding: 32px; + box-shadow: 0 0 1px 5px rgba(0, 0, 0, 0.2); + border-radius: 8px; + overflow: hidden; + transform: translate(-50%, -50%); + z-index: 951; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; + + .header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + .icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 150px; + height: 40px; + background: #fff; + border-radius: 4px; + margin-right: $margin-md; + + img { + display: block; + } + } + + .title { + @include barlow-semi-bold; + + flex: 1; + font-size: 22px; + line-height: 26px; + color: $tco-black; + text-transform: uppercase; + } + + .close-button { + cursor: pointer; + height: 15px; + width: 15px; + } + } + + .body { + flex: 1; + width: 100%; + display: flex; + flex-direction: column; + } + + .divider { + width: 100%; + height: 2px; + background-color: #e9e9e9; + border-radius: 1px; + } + + .footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + + .left-button { + @include roboto-bold; + + text-align: center; + margin: 0; + height: 48px; + font-size: 16px; + line-height: 21px; + letter-spacing: 0.008em; + text-transform: uppercase; + color: $color-turq-160; + padding: 12px 24px; + background: $tc-white; + border: 2px solid $color-turq-160; + border-radius: 50px; + cursor: pointer; + + &.disabled { + background: #f4f4f4; + color: #767676; + border: none; + line-height: 24px; + pointer-events: none; + } + } + + .right-button { + @include roboto-bold; + + text-align: center; + margin: 0; + height: 48px; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.008em; + text-transform: uppercase; + color: $tc-white; + padding: 12px 24px; + background: $color-turq-160; + border-radius: 50px; + cursor: pointer; + + &.disabled { + background: #f4f4f4; + color: #767676; + pointer-events: none; + } + } + } +} diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx new file mode 100644 index 0000000000..11d7d20ada --- /dev/null +++ b/src/shared/components/Settings/Account/Security/index.jsx @@ -0,0 +1,388 @@ +import React, { useState, useEffect, useRef } from 'react'; +import PT from 'prop-types'; +import _ from 'lodash'; +import { config } from 'topcoder-react-utils'; +import QRCode from 'react-qr-code'; +import { UserManager } from 'oidc-client'; +import { SettingBannerV2 as Collapse } from 'components/Settings/SettingsBanner'; +import MfaImage from 'assets/images/account/security/mfa.svg'; +import DiceLogo from 'assets/images/account/security/dicelogo.png'; +import DiceLogoBig from 'assets/images/account/security/dicelogobig.png'; +import GooglePlay from 'assets/images/account/security/google-play.svg'; +import AppleStore from 'assets/images/account/security/apple-store.svg'; +import UnsuccessfulIcon from 'assets/images/account/security/unsuccessful.svg'; +import Modal from './Modal'; + + +import './styles.scss'; + +export default function Security({ + usermfa, getUser2fa, updateUser2fa, updateUserDice, getNewDiceConnection, + getDiceConnection, tokenV3, handle, emailAddress, +}) { + const [setupStep, setSetupStep] = useState(-1); + const [isConnVerifyRunning, setIsConnVerifyRunning] = useState(false); + const [connVerifyCounter, setConnVerifyCounter] = useState(0); + const useInterval = (callback, delay) => { + const savedCallback = useRef(); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // eslint-disable-next-line consistent-return + useEffect(() => { + function tick() { + savedCallback.current(); + } + if (delay !== null) { + const id = setInterval(tick, delay); + return () => clearInterval(id); + } + setConnVerifyCounter(0); + }, [delay]); + }; + + const getMfaOption = () => { + const mfaEnabled = _.get(usermfa, 'user2fa.mfaEnabled'); + if (mfaEnabled) return true; + return false; + }; + + const getDiceOption = () => { + const diceEnabled = _.get(usermfa, 'user2fa.diceEnabled'); + if (diceEnabled) return true; + return false; + }; + + const mfaChecked = getMfaOption(); + const diceChecked = getDiceOption(); + const userId = _.get(usermfa, 'user2fa.userId'); + const diceConnection = _.get(usermfa, 'diceConnection'); + + const onUpdateMfaOption = () => { + if (usermfa.updatingUser2fa) { + return; + } + updateUser2fa(userId, !mfaChecked, tokenV3); + }; + + const goToConnection = () => { + if (mfaChecked && !usermfa.gettingNewDiceConnection) { + getNewDiceConnection(userId, tokenV3); + } + setSetupStep(1); + setIsConnVerifyRunning(true); + }; + + const getConnectionAccepted = () => { + if (diceConnection.accepted) return true; + return false; + }; + + const openSetup = () => { + setSetupStep(0); + }; + + const closeSetup = () => { + setSetupStep(-1); + }; + + const verifyConnection = () => { + if (getConnectionAccepted() || usermfa.diceConnectionError) { + setIsConnVerifyRunning(false); + } else if (!usermfa.gettingDiceConnection && diceConnection.id) { + if (connVerifyCounter >= 60) { + closeSetup(); + } else { + getDiceConnection(userId, diceConnection.id, tokenV3); + setConnVerifyCounter(connVerifyCounter + 1); + } + } + }; + + const verificationPopup = () => { + const diceUrl = config.DICE_VERIFIER_URL; + const accountsAuth = config.URL.AUTH; + const manager = new UserManager({ + authority: diceUrl, + client_id: 'topcoder', + response_type: 'code', + scope: 'openid profile vc_authn', + popup_redirect_uri: `${accountsAuth}/dice-verify-callback.html`, + response_mode: 'query', + loadUserInfo: false, + popupWindowFeatures: 'location=no,toolbar=no,menubar=no,width=1000,height=611,left=100,top=100', + }); + manager.settings.metadata = { + issuer: diceUrl, + jwks_uri: `${diceUrl}/.well-known/openid-configuration/jwks`, + authorization_endpoint: `${diceUrl}/vc/connect/authorize?pres_req_conf_id=Topcoder_2FA_Validate_Cred`, + token_endpoint: `${diceUrl}/vc/connect/token`, + userinfo_endpoint: `${diceUrl}/connect/userinfo`, + check_session_iframe: `${diceUrl}/vc/connect/checksession`, + revocation_endpoint: `${diceUrl}/vc/connect/revocation`, + }; + + manager.signinPopup().then( + (user) => { + const userEmail = _.get(user, 'profile.Email'); + if (!_.isUndefined(userEmail) && _.lowerCase(userEmail) === _.lowerCase(emailAddress)) { + updateUserDice(userId, true, tokenV3); + setSetupStep(3); + } else { + setSetupStep(4); + } + }, + () => { setSetupStep(4); }, + ); + }; + + const goToVerification = () => { + setSetupStep(2); + verificationPopup(); + }; + + const finishSetup = () => { + getUser2fa(userId, tokenV3); + closeSetup(); + }; + + useInterval(verifyConnection, setupStep === 1 && isConnVerifyRunning ? 5000 : null); + + const setupStepNodes = [ + +
+
+ STEP 1 OF 3 +
+
+ First, please download the DICE ID App from the + Google Play Store or the iOS App Store. +
+
+
+ + +
+
+ + +
+
+
+ After you have downloaded and installed the mobile app, + make sure to complete the configuration process. + When ready, click next below. +
+
+
, + +
+
+ STEP 2 OF 3 +
+
+ Scan the following DICE ID QR Code in your DICE ID mobile application. +
+
+ {diceConnection.connection + ? + : 'Loading'} +
+
+ Once the connection is established, the service will offer you a Verifiable Credential. +
Press the ACCEPT button in your DICE ID App. +
+
+
, + {}} + rightButtonDisabled + > +
+
+ Processing... +
+
+ Please wait while your credentials are validated. +
+
+ diceid +
+
+
+ Powered by DICE ID +
+
, + +
+
+ Setup completed! +
+
+ Hello {handle},

+ Your credentials have been verified and you are all set + for MFA using your decentralized identity (DICE ID). +
+
+ diceid +
+
+ For more information on DICE ID, please visit
+ https://www.diceid.com/

+ Please click Finish bellow. +
+
+
, + +
+
+
+ +
+
+ Unsuccessful Verification +
+
+
+ Hello {handle},

+ Your credentials could not be verified, + you won't be able to connect to MFA using your decentralized identity (DICE ID). +
+
+ diceid +
+
+ Please try again your process after few minutes.

+ Please click Finish bellow. +
+
+
, + ]; + return ( + + {setupStep >= 0 && ( + setupStepNodes[setupStep] + )} +
+ +

+ Security +

+
+
+ +
+
+
+ Multi Factor Authentication (MFA) Status +
+
+ Status of MFA for your Topcoder account. + If enabled, MFA will be enofrced during the Topcoder login process. +
+
+
+ + +
+
+
+
+ diceid +
+
+
+ DICE ID Authenticator App +
+
+ DICE ID authentication application +
+
+ {diceChecked + ? ( +
+ { }} + className="onoffswitch-checkbox" + disabled + /> + +
+ ) + : ( +
+ Setup DICE ID Authentication +
+ ) + } +
+
+
+
+ ); +} + +Security.propTypes = { + usermfa: PT.shape().isRequired, + getUser2fa: PT.func.isRequired, + updateUser2fa: PT.func.isRequired, + updateUserDice: PT.func.isRequired, + getNewDiceConnection: PT.func.isRequired, + getDiceConnection: PT.func.isRequired, + tokenV3: PT.string.isRequired, + handle: PT.string.isRequired, + emailAddress: PT.string.isRequired, +}; diff --git a/src/shared/components/Settings/Account/Security/styles.scss b/src/shared/components/Settings/Account/Security/styles.scss new file mode 100644 index 0000000000..3740a880fb --- /dev/null +++ b/src/shared/components/Settings/Account/Security/styles.scss @@ -0,0 +1,226 @@ +@import "../../style"; +@import "~styles/mixins"; + +.security-container { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: $pad-xxxxl; + background-color: $color-tc-white; + border-radius: 4px; + gap: 32px; + + @include upto-sm { + padding: 26px 16px; + gap: 24px; + } + + .security-title { + @include barlow-semi-bold; + + font-size: 20px; + line-height: 22px; + color: #2a2a2a; + text-transform: uppercase; + width: 100%; + } + + .factor-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: $pad-lg; + border: 1px solid $color-black-20; + border-radius: 8px; + gap: 16px; + width: 100%; + + @include upto-sm { + gap: 8px; + flex-direction: column; + align-items: flex-start; + } + + .icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 74px; + width: 74px; + height: 74px; + background: $color-black-5; + border-radius: 4px; + + @include upto-sm { + align-self: flex-start; + } + + img { + display: block; + } + } + + .info { + display: flex; + flex-direction: column; + flex: 1; + + @include upto-sm { + gap: 8px; + } + + .info-first-line { + @include roboto-regular; + + font-size: 16px; + line-height: 20px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: capitalize; + color: $tco-black; + } + + .info-second-line { + @include roboto-regular; + + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: $color-black-60; + + @include upto-sm { + padding-right: 0; + } + } + } + + .on-off-switch { + @include upto-sm { + display: block; + position: absolute; + align-self: flex-end; + margin-top: 10px; + } + } + + .disabled-toggle { + cursor: not-allowed; + } + + .button { + @include roboto-bold; + + text-align: center; + margin: 0; + height: 48px; + font-size: 16px; + line-height: 21px; + letter-spacing: 0.008em; + text-transform: uppercase; + color: $color-turq-160; + padding: 12px 24px; + background: $tc-white; + border: 2px solid $color-turq-160; + border-radius: 50px; + cursor: pointer; + + &.disabled { + background: #f4f4f4; + color: #767676; + border: none; + line-height: 24px; + pointer-events: none; + } + + @include upto-sm { + display: none; + } + } + } +} + +.step-body { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + + .step-title-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .icon-unsuccessful { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 32px; + width: 32px; + height: 32px; + + svg { + display: block; + } + } + } + + .step-title { + @include barlow-condensed-medium; + + font-size: 26px; + line-height: 28px; + color: $tco-black; + text-transform: uppercase; + + &.error { + color: #ef476f; + } + } + + .step-content { + @include roboto-regular; + + font-size: 20px; + line-height: 26px; + color: $tco-black; + } + + .app-store { + display: flex; + flex-direction: row; + padding: 16px 0; + align-items: flex-start; + gap: 100px; + width: 100%; + justify-content: center; + + .market { + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; + } + } + + .body-logo { + display: flex; + flex-direction: row; + padding: 57px 0; + align-items: flex-start; + gap: 100px; + width: 100%; + justify-content: center; + } +} + +.step-footer { + @include roboto-regular; + + font-size: 12px; + line-height: 18px; + text-align: center; + color: #767676; + margin-top: auto; +} diff --git a/src/shared/components/Settings/Account/index.jsx b/src/shared/components/Settings/Account/index.jsx index e772b96a9f..01cefef021 100644 --- a/src/shared/components/Settings/Account/index.jsx +++ b/src/shared/components/Settings/Account/index.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PT from 'prop-types'; import { PrimaryButton } from 'topcoder-react-ui-kit'; import MyAccount from './MyAccount'; +import Security from './Security'; import ErrorWrapper from '../ErrorWrapper'; import styles from './styles.scss'; @@ -78,6 +79,9 @@ export default class Account extends React.Component { {...this.props} ref={this.myAccountRef} /> +
{saveBtn}
@@ -87,7 +91,7 @@ export default class Account extends React.Component { Account.defaultProps = { isSaving: false, - setIsSaving: () => {}, + setIsSaving: () => { }, }; Account.propTypes = { diff --git a/src/shared/containers/Settings.jsx b/src/shared/containers/Settings.jsx index d567e24db8..a8b8dff455 100644 --- a/src/shared/containers/Settings.jsx +++ b/src/shared/containers/Settings.jsx @@ -12,6 +12,7 @@ import { goToLogin } from 'utils/tc'; import { actions } from 'topcoder-react-lib'; import settingsActions, { TABS } from 'actions/page/settings'; import settingsUIActions from 'actions/page/ui'; +import mfaActions from 'actions/mfa'; import Error404 from 'components/Error404'; import LoadingIndicator from 'components/LoadingIndicator'; @@ -123,6 +124,7 @@ function mapStateToProps(state) { settingsPageState: state.page.settings, authenticating: state.auth.authenticating, handle: _.get(state.auth, 'user.handle'), + emailAddress: _.defaults(_.get(state.auth, 'user.email'), _.get(state.auth, 'profile.email')), tokenV3: state.auth.tokenV3, profile: state.auth.profile, user: state.auth.user, @@ -134,6 +136,7 @@ function mapStateToProps(state) { traitRequestCount: state.settings.traitRequestCount, userTraits: state.settings.userTraits, skills: state.profile.skills, + usermfa: state.usermfa, }; } @@ -170,6 +173,11 @@ function mapDispatchToProps(dispatch) { } else if (user.userId) { dispatch(profileActions.getCredentialDone(user, tokenV3)); } + if (profile.userId) { + dispatch(mfaActions.usermfa.getUser2faDone(profile.userId, tokenV3)); + } else if (user.userId) { + dispatch(mfaActions.usermfa.getUser2faDone(user.userId, tokenV3)); + } } else if (settingsTab === TABS.TOOLS) { dispatch(lookupActions.getTypesInit()); dispatch(lookupActions.getTypesDone()); @@ -181,7 +189,7 @@ function mapDispatchToProps(dispatch) { selectTab: tab => dispatch(settingsActions.page.settings.selectTab(tab)), clearIncorrectPassword: () => dispatch(settingsActions.page.settings.clearIncorrectPassword()), clearToastrNotification: - (() => dispatch(settingsActions.page.settings.clearToastrNotification())), + (() => dispatch(settingsActions.page.settings.clearToastrNotification())), addWebLink: (handle, tokenV3, webLink) => { dispatch(profileActions.addWebLinkInit()); dispatch(profileActions.addWebLinkDone(handle, tokenV3, webLink)); @@ -287,6 +295,24 @@ function mapDispatchToProps(dispatch) { updateEmailConflict: (state) => { dispatch(actions.profile.updateEmailConflict(state)); }, + getUser2fa: (userId, tokenV3) => { + dispatch(mfaActions.usermfa.getUser2faDone(userId, tokenV3)); + }, + updateUser2fa: (userId, mfaEnabled, tokenV3) => { + dispatch(mfaActions.usermfa.updateUser2faInit()); + dispatch(mfaActions.usermfa.updateUser2faDone(userId, mfaEnabled, tokenV3)); + }, + updateUserDice: (userId, diceEnabled, tokenV3) => { + dispatch(mfaActions.usermfa.updateUserDiceDone(userId, diceEnabled, tokenV3)); + }, + getNewDiceConnection: (userId, tokenV3) => { + dispatch(mfaActions.usermfa.getNewDiceConnectionInit()); + dispatch(mfaActions.usermfa.getNewDiceConnectionDone(userId, tokenV3)); + }, + getDiceConnection: (userId, connectionId, tokenV3) => { + dispatch(mfaActions.usermfa.getDiceConnectionInit()); + dispatch(mfaActions.usermfa.getDiceConnectionDone(userId, connectionId, tokenV3)); + }, }; } diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index ab8dc25019..c534383ae7 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -36,6 +36,7 @@ import { factory as tcCommunitiesFactory } from './tc-communities'; import { factory as leaderboardFactory } from './leaderboard'; import { factory as scoreboardFactory } from './tco/scoreboard'; import { factory as termsFactory } from './terms'; +import { factory as mfaFactory } from './mfa'; import newsletterPreferences from './newsletterPreferences'; import mmLeaderboard from './mmLeaderboard'; import tcoLeaderboards from './tco/leaderboards'; @@ -143,6 +144,7 @@ export function factory(req) { leaderboard: leaderboardFactory(req), scoreboard: scoreboardFactory(req), terms: termsFactory(req), + usermfa: mfaFactory(req), page: pageFactory(req), }).then(resolvedReducers => redux.combineReducers((state) => { const res = { ...state }; diff --git a/src/shared/reducers/mfa.js b/src/shared/reducers/mfa.js new file mode 100644 index 0000000000..7798594779 --- /dev/null +++ b/src/shared/reducers/mfa.js @@ -0,0 +1,142 @@ +/** + * Reducer for dashboard + */ + +import actions from 'actions/mfa'; +import { handleActions } from 'redux-actions'; +import { logger, errors } from 'topcoder-react-lib'; +import _ from 'lodash'; + +/** + * Handles USERMFA/GET_USER2FA_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onGetUser2faDone(state, { payload, error }) { + if (error) { + logger.error('Failed to get user 2fa settings', payload); + errors.fireErrorMessage('Failed to get user 2fa settings', payload); + return { ...state }; + } + + return ({ + ...state, + user2fa: payload, + }); +} + +/** + * Handles USERMFA/UPDATE_USER2FA_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onUpdateUser2faDone(state, { payload, error }) { + if (error) { + logger.error('Failed to update user 2fa settings', payload); + errors.fireErrorMessage('Failed to update user 2fa settings', payload); + return { ...state }; + } + + return ({ + ...state, + user2fa: payload, + updatingUser2fa: false, + }); +} + +/** + * Handles USERMFA/UPDATE_USER_DICE_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onUpdateUserDiceDone(state, { payload, error }) { + if (error) { + logger.error('Failed to update user dice settings', payload); + errors.fireErrorMessage('Failed to update user dice settings', payload); + return { ...state }; + } + + return ({ + ...state, + user2fa: payload, + }); +} + +/** + * Handles USERMFA/GET_NEW_DICE_CONNECTION_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onGetNewDiceConnectionDone(state, { payload, error }) { + if (error) { + logger.error('Failed to get new dice connection', payload); + errors.fireErrorMessage('Failed to get new dice connection', payload); + return { ...state }; + } + + return ({ + ...state, + diceConnection: payload, + gettingNewDiceConnection: false, + diceConnectionError: false, + }); +} + +/** + * Handles USERMFA/GET_DICE_CONNECTION_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onGetDiceConnectionDone(state, { payload, error }) { + if (error) { + logger.error('Failed to get dice connection', payload); + errors.fireErrorMessage('Failed to get dice connection', payload); + return { ...state, diceConnectionError: true }; + } + + return ({ + ...state, + diceConnection: payload, + gettingDiceConnection: false, + }); +} + +/** + * Creates a new mfa reducer with the specified initial state. + * @param {Object} initialState Optional. Initial state. + * @return {Function} mfa reducer. + */ +function create(initialState) { + const a = actions.usermfa; + return handleActions({ + [a.getUser2faDone]: onGetUser2faDone, + [a.updateUser2faInit]: state => ({ ...state, updatingUser2fa: true }), + [a.updateUser2faDone]: onUpdateUser2faDone, + [a.updateUserDiceDone]: onUpdateUserDiceDone, + [a.getNewDiceConnectionInit]: state => ( + { ...state, diceConnection: {}, gettingNewDiceConnection: true }), + [a.getNewDiceConnectionDone]: onGetNewDiceConnectionDone, + [a.getDiceConnectionInit]: state => ({ ...state, gettingDiceConnection: true }), + [a.getDiceConnectionDone]: onGetDiceConnectionDone, + }, _.defaults(initialState, { + user2fa: {}, + diceConnection: {}, + })); +} + +/** + * Factory which creates a new reducer. + * @return {Promise} + * @resolves {Function(state, action): state} New reducer. + */ +export function factory() { + return Promise.resolve(create()); +} + +/* Default reducer with empty initial state. */ +export default create(); diff --git a/src/shared/services/mfa.js b/src/shared/services/mfa.js new file mode 100644 index 0000000000..6a6062c338 --- /dev/null +++ b/src/shared/services/mfa.js @@ -0,0 +1,93 @@ + +import { services, tc } from 'topcoder-react-lib'; + +const { getApi } = services.api; + +class MfaService { + /** + * @param {String} tokenV3 Auth token for Topcoder API v3. + */ + constructor(tokenV3) { + this.private = { + api: getApi('V3', tokenV3), + tokenV3, + }; + } + + /** + * Gets user 2fa settings. + * @param {Number} userId user id + * @return {Promise} Resolves to the user 2fa settings. + */ + async getUser2fa(userId) { + const res = await this.private.api.get(`/users/${userId}/2fa`); + return tc.getApiResponsePayload(res); + } + + /** + * Update user 2fa settings + * @param {Number} userId User id. + * @param {Boolean} mfaEnabled 2fa data. + * @return {Promise} Resolves to the user 2fa settings. + */ + async updateUser2fa(userId, mfaEnabled) { + const settings = { + mfaEnabled, + }; + + const res = await this.private.api.patchJson(`/users/${userId}/2fa`, { param: settings }); + return tc.getApiResponsePayload(res); + } + + /** + * Update user dice settings + * @param {Number} userId User id. + * @param {Boolean} diceEnabled dice flag. + * @return {Promise} Resolves to the user 2fa settings. + */ + async updateUserDice(userId, diceEnabled) { + const settings = { + diceEnabled, + }; + + const res = await this.private.api.patchJson(`/users/${userId}/2fa`, { param: settings }); + return tc.getApiResponsePayload(res); + } + + /** + * Gets new dice connection. + * @param {Number} userId user id + * @return {Promise} Resolves to the dice connection. + */ + async getNewDiceConnection(userId) { + const res = await this.private.api.get(`/users/${userId}/diceConnection`); + return tc.getApiResponsePayload(res); + } + + /** + * Gets dice connection. + * @param {Number} userId user id + * @param {Number} connectionId user id + * @return {Promise} Resolves to the dice connection. + */ + async getDiceConnection(userId, connectionId) { + const res = await this.private.api.get(`/users/${userId}/diceConnection/${connectionId}`); + return tc.getApiResponsePayload(res); + } +} + +let lastInstance = null; +/** + * Returns a new or existing lookup service. + * @param {String} tokenV3 Optional. Auth token for Topcoder API v3. + * @return {MfaService} Mfa service object + */ +export function getService(tokenV3) { + if (!lastInstance || tokenV3 !== lastInstance.private.tokenV3) { + lastInstance = new MfaService(tokenV3); + } + return lastInstance; +} + +/* Using default export would be confusing in this case. */ +export default undefined; From 8163d5945b88824e130541aa5fcd9c9d20729235 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Sun, 25 Sep 2022 13:54:37 -0300 Subject: [PATCH 02/15] ci: deploy feature/dice-setup to QA env --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1ed1928599..e783858018 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -363,7 +363,7 @@ workflows: filters: branches: only: - - reskin-profile-settings + - feature/dice-setup # This is beta env for production soft releases - "build-prod-beta": context : org-global From fbc3e3c9598fe44af3dc3b15c8356f79a69ccda4 Mon Sep 17 00:00:00 2001 From: Emre Date: Mon, 26 Sep 2022 16:24:07 +0300 Subject: [PATCH 03/15] fix dice setup modal --- .../components/Settings/Account/Security/Modal/style.scss | 5 +++-- src/shared/components/Settings/Account/Security/index.jsx | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/shared/components/Settings/Account/Security/Modal/style.scss b/src/shared/components/Settings/Account/Security/Modal/style.scss index 2ffd61c795..b5eb75153a 100644 --- a/src/shared/components/Settings/Account/Security/Modal/style.scss +++ b/src/shared/components/Settings/Account/Security/Modal/style.scss @@ -24,13 +24,14 @@ padding: 32px; box-shadow: 0 0 1px 5px rgba(0, 0, 0, 0.2); border-radius: 8px; - overflow: hidden; + overflow: auto; transform: translate(-50%, -50%); z-index: 951; display: flex; flex-direction: column; align-items: flex-start; gap: 24px; + max-height: 99%; .header { display: flex; @@ -79,7 +80,7 @@ .divider { width: 100%; - height: 2px; + min-height: 2px; background-color: #e9e9e9; border-radius: 1px; } diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx index 11d7d20ada..63f720cbb0 100644 --- a/src/shared/components/Settings/Account/Security/index.jsx +++ b/src/shared/components/Settings/Account/Security/index.jsx @@ -139,6 +139,9 @@ export default function Security({ }; const goToVerification = () => { + if (!getConnectionAccepted()) { + return; + } setSetupStep(2); verificationPopup(); }; @@ -309,7 +312,7 @@ export default function Security({
Status of MFA for your Topcoder account. - If enabled, MFA will be enofrced during the Topcoder login process. + If enabled, MFA will be enforced during the Topcoder login process.
From f010990985f4586e346bbb7ead50773490386316 Mon Sep 17 00:00:00 2001 From: Emre Date: Mon, 26 Sep 2022 20:08:00 +0300 Subject: [PATCH 04/15] add dice callback page --- src/server/index.js | 2 ++ src/server/static/dice-signin-callback.html | 21 +++++++++++++++++++ .../Settings/Account/Security/index.jsx | 7 +++++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/server/static/dice-signin-callback.html diff --git a/src/server/index.js b/src/server/index.js index 70590aa8b8..d9b7297276 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -313,6 +313,8 @@ async function onExpressJsSetup(server) { * page would do on signing / rejecting a document. */ server.use('/community-app-assets/api/mock/docu-sign', (req, res) => setTimeout(() => res.send(mockDocuSignFactory(req.query.returnUrl)), 3000)); + server.use('/community-app-assets/dice-signin-callback.html', (req, res) => res.sendFile(path.resolve(__dirname, './static/dice-signin-callback.html'))); + /* TODO: * This is a temporary fallback route: some of the assets in the app are not * properly packed with Webpack, and they rely on just being copied into some diff --git a/src/server/static/dice-signin-callback.html b/src/server/static/dice-signin-callback.html new file mode 100644 index 0000000000..1b7e64b49c --- /dev/null +++ b/src/server/static/dice-signin-callback.html @@ -0,0 +1,21 @@ + + + + + Processing verfiable credentials + + + + + +

Verifying credentials....

+ + + + + + \ No newline at end of file diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx index 63f720cbb0..ecfb55e51b 100644 --- a/src/shared/components/Settings/Account/Security/index.jsx +++ b/src/shared/components/Settings/Account/Security/index.jsx @@ -103,13 +103,16 @@ export default function Security({ const verificationPopup = () => { const diceUrl = config.DICE_VERIFIER_URL; - const accountsAuth = config.URL.AUTH; + let baseRedirectUrl = config.URL.BASE; + if (baseRedirectUrl.indexOf('-dev') !== -1) { + baseRedirectUrl = window.location.origin; + } const manager = new UserManager({ authority: diceUrl, client_id: 'topcoder', response_type: 'code', scope: 'openid profile vc_authn', - popup_redirect_uri: `${accountsAuth}/dice-verify-callback.html`, + popup_redirect_uri: `${baseRedirectUrl}/community-app-assets/dice-signin-callback.html`, response_mode: 'query', loadUserInfo: false, popupWindowFeatures: 'location=no,toolbar=no,menubar=no,width=1000,height=611,left=100,top=100', From dab43538e798c767cff6466b661fffda96c9cb0c Mon Sep 17 00:00:00 2001 From: Emre Date: Tue, 27 Sep 2022 16:19:36 +0300 Subject: [PATCH 05/15] add new verifier page --- config/default.js | 2 +- config/production.js | 2 +- src/server/index.js | 2 - src/server/static/dice-signin-callback.html | 21 ----- .../Settings/Account/Security/Modal/index.jsx | 64 +++++++------- .../Security/VerificationListener/index.jsx | 23 +++++ .../Settings/Account/Security/index.jsx | 86 ++++++------------- 7 files changed, 82 insertions(+), 118 deletions(-) delete mode 100644 src/server/static/dice-signin-callback.html create mode 100644 src/shared/components/Settings/Account/Security/VerificationListener/index.jsx diff --git a/config/default.js b/config/default.js index 31e4750436..e0e8dcc8a6 100644 --- a/config/default.js +++ b/config/default.js @@ -457,5 +457,5 @@ module.exports = { ENABLE_BADGE_UI: true, }, PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com', - DICE_VERIFIER_URL: 'https://tc-vcauth-uat.diceid.com', + DICE_VERIFY_URL: 'https://accounts-auth0.topcoder-dev.com', }; diff --git a/config/production.js b/config/production.js index c365acd067..e97b53d4d2 100644 --- a/config/production.js +++ b/config/production.js @@ -227,5 +227,5 @@ module.exports = { ENABLE_RECOMMENDER: true, PLATFORM_SITE_URL: 'https://platform.topcoder.com', PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder.com', - DICE_VERIFIER_URL: 'https://tc-vcauth.diceid.com', + DICE_VERIFY_URL: 'https://accounts-auth0.topcoder.com', }; diff --git a/src/server/index.js b/src/server/index.js index d9b7297276..70590aa8b8 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -313,8 +313,6 @@ async function onExpressJsSetup(server) { * page would do on signing / rejecting a document. */ server.use('/community-app-assets/api/mock/docu-sign', (req, res) => setTimeout(() => res.send(mockDocuSignFactory(req.query.returnUrl)), 3000)); - server.use('/community-app-assets/dice-signin-callback.html', (req, res) => res.sendFile(path.resolve(__dirname, './static/dice-signin-callback.html'))); - /* TODO: * This is a temporary fallback route: some of the assets in the app are not * properly packed with Webpack, and they rely on just being copied into some diff --git a/src/server/static/dice-signin-callback.html b/src/server/static/dice-signin-callback.html deleted file mode 100644 index 1b7e64b49c..0000000000 --- a/src/server/static/dice-signin-callback.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Processing verfiable credentials - - - - - -

Verifying credentials....

- - - - - - \ No newline at end of file diff --git a/src/shared/components/Settings/Account/Security/Modal/index.jsx b/src/shared/components/Settings/Account/Security/Modal/index.jsx index c9324d2f3c..ace4322fa1 100644 --- a/src/shared/components/Settings/Account/Security/Modal/index.jsx +++ b/src/shared/components/Settings/Account/Security/Modal/index.jsx @@ -6,7 +6,7 @@ import CloseButton from 'assets/images/account/security/green-close.svg'; import './style.scss'; export default function DiceModal({ - children, onCancel, leftButtonName, leftButtonDisabled, leftButtonClick, + showTools, children, onCancel, leftButtonName, leftButtonDisabled, leftButtonClick, rightButtonName, rightButtonDisabled, rightButtonClick, }) { useEffect(() => { @@ -21,37 +21,37 @@ export default function DiceModal({ styleName="container" onWheel={event => event.stopPropagation()} > -
-
- diceid -
-
DICE ID authenticator setup
- -
-
-
+ {showTools ? ( + <>
+
+ diceid +
+
DICE ID authenticator setup
+ +
+ {children} +
+
{leftButtonName} +
+
{rightButtonName} +
+
+ ) + : ( {children} -
- -
-
-
{leftButtonName} -
-
{rightButtonName} -
-
+ )}
{ + if (e.source.indexOf(source) !== -1) { + callback(e.data); + } + }, [source]) + + useEffect(() => { + window.addEventListener(event, messageHandler); + return () => window.removeEventListener(event, messageHandler); + }, [event, messageHandler]); + + return false; +} + +DiceModal.propTypes = { + event: PT.string.isRequired, + callback: PT.func.isRequired, + source: PT.string.isRequired, +}; diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx index ecfb55e51b..9ea43af178 100644 --- a/src/shared/components/Settings/Account/Security/index.jsx +++ b/src/shared/components/Settings/Account/Security/index.jsx @@ -3,7 +3,6 @@ import PT from 'prop-types'; import _ from 'lodash'; import { config } from 'topcoder-react-utils'; import QRCode from 'react-qr-code'; -import { UserManager } from 'oidc-client'; import { SettingBannerV2 as Collapse } from 'components/Settings/SettingsBanner'; import MfaImage from 'assets/images/account/security/mfa.svg'; import DiceLogo from 'assets/images/account/security/dicelogo.png'; @@ -12,6 +11,7 @@ import GooglePlay from 'assets/images/account/security/google-play.svg'; import AppleStore from 'assets/images/account/security/apple-store.svg'; import UnsuccessfulIcon from 'assets/images/account/security/unsuccessful.svg'; import Modal from './Modal'; +import VerificationListener from './VerificationListener'; import './styles.scss'; @@ -23,6 +23,7 @@ export default function Security({ const [setupStep, setSetupStep] = useState(-1); const [isConnVerifyRunning, setIsConnVerifyRunning] = useState(false); const [connVerifyCounter, setConnVerifyCounter] = useState(0); + const diceVerifyUrl = config.DICE_VERIFY_URL; const useInterval = (callback, delay) => { const savedCallback = useRef(); @@ -101,54 +102,23 @@ export default function Security({ } }; - const verificationPopup = () => { - const diceUrl = config.DICE_VERIFIER_URL; - let baseRedirectUrl = config.URL.BASE; - if (baseRedirectUrl.indexOf('-dev') !== -1) { - baseRedirectUrl = window.location.origin; - } - const manager = new UserManager({ - authority: diceUrl, - client_id: 'topcoder', - response_type: 'code', - scope: 'openid profile vc_authn', - popup_redirect_uri: `${baseRedirectUrl}/community-app-assets/dice-signin-callback.html`, - response_mode: 'query', - loadUserInfo: false, - popupWindowFeatures: 'location=no,toolbar=no,menubar=no,width=1000,height=611,left=100,top=100', - }); - manager.settings.metadata = { - issuer: diceUrl, - jwks_uri: `${diceUrl}/.well-known/openid-configuration/jwks`, - authorization_endpoint: `${diceUrl}/vc/connect/authorize?pres_req_conf_id=Topcoder_2FA_Validate_Cred`, - token_endpoint: `${diceUrl}/vc/connect/token`, - userinfo_endpoint: `${diceUrl}/connect/userinfo`, - check_session_iframe: `${diceUrl}/vc/connect/checksession`, - revocation_endpoint: `${diceUrl}/vc/connect/revocation`, - }; - - manager.signinPopup().then( - (user) => { - const userEmail = _.get(user, 'profile.Email'); - if (!_.isUndefined(userEmail) && _.lowerCase(userEmail) === _.lowerCase(emailAddress)) { - updateUserDice(userId, true, tokenV3); - setSetupStep(3); - } else { - setSetupStep(4); - } - }, - () => { setSetupStep(4); }, - ); - }; - const goToVerification = () => { if (!getConnectionAccepted()) { return; } setSetupStep(2); - verificationPopup(); }; + const verificationCallback = (data) => { + const userEmail = _.get(data, 'profile.Email'); + if (!_.isUndefined(userEmail) && _.lowerCase(userEmail) === _.lowerCase(emailAddress)) { + updateUserDice(userId, true, tokenV3); + setSetupStep(3); + } else { + setSetupStep(4); + } + } + const finishSetup = () => { getUser2fa(userId, tokenV3); closeSetup(); @@ -216,27 +186,19 @@ export default function Security({
, {}} - rightButtonDisabled + showTools={false} + onCancel={()=>{}} + leftButtonName="" + leftButtonClick={()=>{}} + rightButtonName="" + rightButtonClick={()=>{}} > -
-
- Processing... -
-
- Please wait while your credentials are validated. -
-
- diceid -
-
-
- Powered by DICE ID -
+