diff --git a/apps/console/package.json b/apps/console/package.json index a3171cdfa0d..d513c3ed3df 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -42,13 +42,14 @@ "@microsoft/applicationinsights-core-js": "^3.0.0", "@microsoft/applicationinsights-react-js": "^3.4.2", "@microsoft/applicationinsights-web": "^3.0.0", + "@monaco-editor/react": "^4.5.1", "@mui/icons-material": "^5.11.16", "@mui/lab": "5.0.0-alpha.129", "@mui/material": "^5.13.0", "@mui/system": "^5.12.3", "@mui/utils": "^5.12.3", - "@oxygen-ui/react": "^1.1.11", - "@oxygen-ui/react-icons": "^1.1.11", + "@oxygen-ui/react": "^1.3.0", + "@oxygen-ui/react-icons": "^1.3.0", "@wso2is/access-control": "^2.0.2", "@wso2is/core": "^2.0.1", "@wso2is/form": "^2.0.0", @@ -83,6 +84,7 @@ "react-notification-system": "^0.4.0", "react-redux": "^7.2.9", "react-router-dom": "^4.3.1", + "reactflow": "^11.7.2", "reactour": "^1.18.0", "recharts": "^2.6.2", "reduce-reducers": "^1.0.4", diff --git a/apps/console/src/features/applications/api/application.ts b/apps/console/src/features/applications/api/application.ts index 7d80b9fbd92..c0f39fa5cce 100644 --- a/apps/console/src/features/applications/api/application.ts +++ b/apps/console/src/features/applications/api/application.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import { AsgardeoSPAClient, HttpClientInstance, HttpRequestConfig } from "@asgardeo/auth-react"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { HttpMethods } from "@wso2is/core/models"; @@ -27,6 +28,7 @@ import useRequest, { import { store } from "../../core/store"; import { ApplicationManagementConstants } from "../constants"; import { + AdaptiveAuthTemplateCategoryListItemInterface, AdaptiveAuthTemplatesListInterface, ApplicationBasicInterface, ApplicationInterface, @@ -764,6 +766,50 @@ export const getAdaptiveAuthTemplates = (): Promise(): RequestResultInterface => { + const requestConfig: RequestConfigInterface = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + url: `${store.getState().config.endpoints.applications}/meta/adaptive-auth-templates` + }; + + const { + data, + error, + isValidating, + mutate + } = useRequest(requestConfig); + + let parsed: Data | undefined = undefined; + + if ((data as AdaptiveAuthTemplatesListInterface)?.templatesJSON) { + try { + parsed = JSON.parse((data as any).templatesJSON); + } catch(e) { + parsed = undefined; + } + } + + return { + data: parsed, + error, + isLoading: !error && !data, + isValidating, + mutate + }; +}; + /** * Get Application Template data. * diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/context/authentication-flow-context.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/context/authentication-flow-context.tsx deleted file mode 100644 index 87d8ede79c5..00000000000 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/context/authentication-flow-context.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Context, createContext } from "react"; - -/** - * Props interface for the AuthenticationFlowContext. - */ -export type AuthenticationFlowContextProps = { - isConditionalAuthenticationEnabled: boolean; - onConditionalAuthenticationToggle: (enabled: boolean) => void; -}; - -/** - * Context object for managing authentication flow status. - */ -const AuthenticationFlowContext: Context = - createContext(null); - -/** - * Display name for the AuthenticationFlowContext. - */ -AuthenticationFlowContext.displayName = "AuthenticationFlowContext"; - -export default AuthenticationFlowContext; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/providers/authentication-flow-provider.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/providers/authentication-flow-provider.tsx deleted file mode 100644 index bbb0aa92e21..00000000000 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/providers/authentication-flow-provider.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { PropsWithChildren, ReactElement, useState } from "react"; -import AuthenticationFlowContext from "../context/authentication-flow-context"; - -export type AuthenticationFlowProviderProps = Record; - -export const AuthenticationFlowProvider = (props: PropsWithChildren): - ReactElement => { - - const { children } = props; - const [ isConditionalAuthenticationEnabled, setIsConditionalAuthenticationEnabled ] = useState(false); - - return ( - setIsConditionalAuthenticationEnabled(enabled) - } } - > - { children } - - ); -}; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx index 31855e79bdf..b8b5aff85e7 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-based-flow.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2020-2023, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import Chip from "@oxygen-ui/react/Chip"; import { FeatureStatus, FeatureTags, useCheckFeatureStatus, useCheckFeatureTags } from "@wso2is/access-control"; import { UIConstants } from "@wso2is/core/constants"; @@ -60,6 +61,7 @@ import { Dispatch } from "redux"; import { Checkbox, Dropdown, Header, Icon, Input, Menu, Sidebar } from "semantic-ui-react"; import { stripSlashes } from "slashes"; import { ScriptTemplatesSidePanel, ScriptTemplatesSidePanelRefInterface } from "./script-templates-side-panel"; +import useAuthenticationFlow from "../../../../../authentication-flow-builder/hooks/use-authentication-flow"; import { AppUtils, EventPublisher, getOperationIcons } from "../../../../../core"; import { OrganizationType } from "../../../../../organizations/constants"; import { OrganizationUtils } from "../../../../../organizations/utils"; @@ -75,7 +77,6 @@ import { AuthenticationSequenceInterface } from "../../../../models"; import { AdaptiveScriptUtils } from "../../../../utils/adaptive-script-utils"; -import UseAuthenticationFlow from "../hooks/use-authentication-flow"; /** * Proptypes for the adaptive scripts component. @@ -185,7 +186,7 @@ export const ScriptBasedFlow: FunctionComponent = const [ showDeleteConfirmationModal, setShowDeleteConfirmationModal ] = useState(false); const [ deletingSecret, setDeletingSecret ] = useState(undefined); - const { isConditionalAuthenticationEnabled, onConditionalAuthenticationToggle } = UseAuthenticationFlow(); + const { isConditionalAuthenticationEnabled, onConditionalAuthenticationToggle } = useAuthenticationFlow(); const eventPublisher: EventPublisher = EventPublisher.getInstance(); @@ -237,8 +238,8 @@ export const ScriptBasedFlow: FunctionComponent = * Check if the feature is a premium. */ useEffect(() => { - if(adaptiveFeatureStatus === FeatureStatus.ENABLED - && adaptiveFeatureTags?.includes(FeatureTags.PREMIUM)) { + if (adaptiveFeatureStatus === FeatureStatus.ENABLED + && adaptiveFeatureTags?.includes(FeatureTags.PREMIUM)) { setIsPremiumFeature(true); } }, []); diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-templates-side-panel.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-templates-side-panel.tsx index 8ff3f182c00..68ffe5528a3 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-templates-side-panel.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/script-based-flow/script-templates-side-panel.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -31,7 +31,8 @@ import React, { } from "react"; import { useTranslation } from "react-i18next"; import { Accordion, Icon, Menu, Segment, Sidebar } from "semantic-ui-react"; -import { TemplateDescription } from "./template-description"; +// eslint-disable-next-line max-len +import AdaptiveAuthTemplateInfoModal from "../../../../../authentication-flow-builder/components/predefined-flows-side-panel/adaptive-auth-template-info-modal"; import { AdaptiveAuthTemplateCategoryInterface, AdaptiveAuthTemplateInterface } from "../../../../models"; /** @@ -218,9 +219,8 @@ export const ScriptTemplatesSidePanel: FunctionComponent { - selectedTemplate && - ( - void; -} - -/** - * The modal that contains template description. - * - * @param props - React component props. - * - * @returns Template Description component. - */ -export const TemplateDescription: FunctionComponent = ( - props: TemplateDescriptionPropsInterface -): ReactElement => { - - const { - template, - open, - onClose - } = props; - - const { t } = useTranslation(); - const { getLink } = useDocumentation(); - - /** - * Resolves the documentation link when a template is selected. - * @returns the resolved documentation link. - */ - const resolveDocumentationLink = (): ReactElement => { - const templateName: string = template?.name; - let docLink: string = undefined; - - if (templateName === AdaptiveAuthTemplateTypes.USER_AGE_BASED) { - docLink = getLink("develop.applications.editApplication.common." + - "signInMethod.conditionalAuthenticaion.template.userAgeBased.learnMore"); - } - - if (templateName === AdaptiveAuthTemplateTypes.GROUP_BASED) { - docLink = getLink("develop.applications.editApplication.common." + - "signInMethod.conditionalAuthenticaion.template.groupBased.learnMore"); - } - - if (templateName === AdaptiveAuthTemplateTypes.IP_BASED) { - docLink = getLink("develop.applications.editApplication.common." + - "signInMethod.conditionalAuthenticaion.template.ipBased.learnMore"); - } - - if (templateName === AdaptiveAuthTemplateTypes.NEW_DEVICE_BASED) { - docLink = getLink("develop.applications.editApplication.common." + - "signInMethod.conditionalAuthenticaion.template.deviceBased.learnMore"); - } - - if (docLink === undefined) { - return null; - } - - return ( - - { t("common:learnMore") } - - ); - - }; - - const getParameters = (): string[] => { - const params : string[] = []; - - if (isObject(template.parametersDescription)) { - Object.entries(template.parametersDescription).map(([ param ]: string[]) => { - params.push(param); - }); - } - - return params; - }; - - const generatePrerequisite = (prerequisite: string, params: string[]): ReactElement => { - const sentenceArray: string[] = prerequisite.split(" "); - const modified: ReactElement[] = []; - let content: string = ""; - - sentenceArray.map((word: string, index: number) => { - if (params.includes(word)) { - modified.push({ content } ); - modified.push( - - { word } - - ); - content = ""; - } else { - content = content.concat(word + " "); - } - }); - modified.push({ content }); - - return ( -

- { modified.map((element: ReactElement) => element) } -

- ); - }; - - return ( - - { template.title } - -

- { template.summary } - { resolveDocumentationLink() } -

- { - Array.isArray(template?.preRequisites) && template.preRequisites.length > 0 && ( - <> -

- { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "templateDescription.description.prerequisites") - } -

- - { template.preRequisites.map((prerequisite: string, index: number) => { - const params: string[] = getParameters(); - - return ( - - - { generatePrerequisite(prerequisite, params) } - - ); - }) } - - - ) - } - { - isObject(template.parametersDescription) && ( - <> -

- { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "templateDescription.description.parameters") - } -

- - - - - { - t("console:develop.features.applications.edit.sections" + - ".signOnMethod.sections.templateDescription.description" + - ".description") - } - - - - { Object.entries(template.parametersDescription) - .map(([ param, description ]: [ string, string ], index: number) => { - return ( - - - { param } - - { description } - - ); - }) } - -
- - ) - } - { - isObject(template.defaultStepsDescription) && ( - <> -

- { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "templateDescription.description.defaultSteps") - } -

- { - Object.entries(template.defaultStepsDescription) - .map(( - [ step, description ]: [ string, string ], - index: number, - steps: [string, string][] - ) => { - return ( -
-
{ index + 1 }
-
{ description }
- { steps.length !== (index + 1) && ( -
-
-
- ) } -
- ); - }) - } - - ) - } - { - template.helpLink && ( - <> -

- { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "templateDescription.description.helpReference") - } -

- - { template.helpLink } - ) - } - /> - - ) - } - { - template.code && ( - <> -

- { - t("console:develop.features.applications.edit.sections.signOnMethod.sections." + - "templateDescription.description.code") - } -

- - - ) - } -
- - { t("common:cancel") } - -
- ); -}; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx index 77461f17089..dc37b655e8d 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/sign-in-method-customization.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2021-2023, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import { AlertLevels, SBACInterface, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { Field, FormValue, Forms } from "@wso2is/forms"; @@ -99,7 +100,6 @@ interface SignInMethodCustomizationPropsInterface extends SBACInterface Promise; } /** @@ -124,7 +124,6 @@ export const SignInMethodCustomization: FunctionComponent, TestableComponentInterface { - /** * Editing application. */ @@ -81,8 +83,8 @@ interface SignOnMethodsPropsInterface extends SBACInterface = ( ): ReactElement => { const { + application, appId, authenticationSequence, clientId, @@ -191,10 +194,6 @@ export const SignOnMethods: FunctionComponent = ( setModeratedAuthenticationSequence(authenticationSequence); }, [ authenticationSequence ]); - const refreshAuthenticators = (): Promise => { - return fetchAndCategorizeAuthenticators(); - }; - /** * Fetches the list of Authenticators and categorize them. * @@ -303,7 +302,7 @@ export const SignOnMethods: FunctionComponent = ( setModeratedAuthenticationSequence(authenticationSequence); } else if (loginFlow === LoginFlowTypes.DEFAULT) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "default" } ); setModeratedAuthenticationSequence({ @@ -312,7 +311,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.SECOND_FACTOR_TOTP) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "second-factor-totp" } ); setModeratedAuthenticationSequence({ @@ -321,7 +320,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.SECOND_FACTOR_EMAIL_OTP) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "second-factor-email-otp" } ); setModeratedAuthenticationSequence({ @@ -330,7 +329,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.SECOND_FACTOR_SMS_OTP) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "second-factor-sms-otp" } ); setModeratedAuthenticationSequence({ @@ -339,7 +338,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.FIDO_LOGIN) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "first-factor-fido" } ); setModeratedAuthenticationSequence({ @@ -348,7 +347,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.GOOGLE_LOGIN) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "google-login" } ); setSocialDisclaimerModalType(LoginFlowTypes.GOOGLE_LOGIN); @@ -379,7 +378,7 @@ export const SignOnMethods: FunctionComponent = ( } } else if (loginFlow === LoginFlowTypes.GITHUB_LOGIN) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "github-login" } ); @@ -443,7 +442,7 @@ export const SignOnMethods: FunctionComponent = ( } } else if (loginFlow === LoginFlowTypes.MICROSOFT_LOGIN) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "microsoft-login" } ); @@ -475,7 +474,7 @@ export const SignOnMethods: FunctionComponent = ( } } else if (loginFlow === LoginFlowTypes.APPLE_LOGIN) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "apple-login" } ); @@ -507,7 +506,7 @@ export const SignOnMethods: FunctionComponent = ( } } else if (loginFlow === LoginFlowTypes.MAGIC_LINK) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "magic-link-login" } ); @@ -517,7 +516,7 @@ export const SignOnMethods: FunctionComponent = ( }); } else if (loginFlow === LoginFlowTypes.EMAIL_OTP) { eventPublisher.publish( - "application-sign-in-method-click-add", + "application-sign-in-method-click-add", { type: "email-otp-login" } ); @@ -827,7 +826,7 @@ export const SignOnMethods: FunctionComponent = ( // If the IDP creation is triggered from the landing page, handle the relevant // login flow changes so that we can navigate the user to the customizing page. if (idpCreateWizardTriggerOrigin === "INTERNAL") { - handleLoginFlowSelect(socialDisclaimerModalType, google, github, facebook, microsoft, + handleLoginFlowSelect(socialDisclaimerModalType, google, github, facebook, microsoft, apple); } }).finally(); @@ -843,28 +842,36 @@ export const SignOnMethods: FunctionComponent = ( }; return ( - - - { - !(isLoading || isAuthenticatorsFetchRequestLoading) ? - ((!readOnly && !loginFlow && isDefaultFlowConfiguration()) - ? ( + fetchAndCategorizeAuthenticators() } + onUpdate={ onUpdate } + isLoading={ isAuthenticatorsFetchRequestLoading } + readOnly={ readOnly } + > + + { !(isLoading || isAuthenticatorsFetchRequestLoading) ? ( + !readOnly && !loginFlow && isDefaultFlowConfiguration() ? ( { - handleLoginFlowSelect(type, + handleLoginFlowSelect( + type, googleAuthenticators, gitHubAuthenticators, facebookAuthenticators, microsoftAuthenticators, - appleAuthenticators); + appleAuthenticators + ); } } data-testid={ `${testId}-landing` } /> ) : ( = ( readOnly={ readOnly } /> ) - ) : - } - { showIDPCreateWizard && renderIDPCreateWizard() } - { showMissingSocialAuthenticatorModal && renderMissingSocialAuthenticatorModal() } - { showDuplicateSocialAuthenticatorSelectionModal - && renderDuplicateSocialAuthenticatorSelectionModal() - } - + ) : ( + + ) } + + ) } + onIDPCreateWizardTrigger={ (type: string, cb: () => void, template: any) => { + setSelectedIDPTemplate(template); + setIDPCreateWizardTriggerOrigin("EXTERNAL"); + setIDPTemplateTypeToTrigger(type); + setShowMissingSocialAuthenticatorModal(false); + setShowIDPCreateWizard(true); + broadcastIDPCreateSuccessMessage = cb; + } } + /> + { showIDPCreateWizard && renderIDPCreateWizard() } + { showMissingSocialAuthenticatorModal && renderMissingSocialAuthenticatorModal() } + { showDuplicateSocialAuthenticatorSelectionModal && + renderDuplicateSocialAuthenticatorSelectionModal() } ); }; diff --git a/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/add-authenticator-modal.tsx b/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/add-authenticator-modal.tsx index 428b4f95d9d..6cc006ebed9 100644 --- a/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/add-authenticator-modal.tsx +++ b/apps/console/src/features/applications/components/settings/sign-on-methods/step-based-flow/add-authenticator-modal.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -15,6 +15,7 @@ * specific language governing permissions and limitations * under the License. */ + import { TestableComponentInterface } from "@wso2is/core/models"; import { EmptyPlaceholder, @@ -29,7 +30,9 @@ import { import classNames from "classnames"; import isEmpty from "lodash-es/isEmpty"; import kebabCase from "lodash-es/kebabCase"; +import orderBy from "lodash-es/orderBy"; import startCase from "lodash-es/startCase"; +import union from "lodash-es/union"; import React, { ChangeEvent, FunctionComponent, @@ -57,19 +60,27 @@ import { } from "semantic-ui-react"; import { Authenticators } from "./authenticators"; import { authenticatorConfig } from "../../../../../../extensions/configs/authenticator"; -import { AppState, EventPublisher, getEmptyPlaceholderIllustrations } from "../../../../../core"; +import { getEmptyPlaceholderIllustrations } from "../../../../../core/configs/ui"; +import { AppState } from "../../../../../core/store"; +import { EventPublisher } from "../../../../../core/utils/event-publisher"; import { getIdPIcons } from "../../../../../identity-providers/configs/ui"; import { IdentityProviderManagementConstants } from "../../../../../identity-providers/constants/identity-provider-management-constants"; +import { AuthenticatorMeta } from "../../../../../identity-providers/meta/authenticator-meta"; import { + AuthenticatorCategories, GenericAuthenticatorInterface, IdentityProviderTemplateCategoryInterface, - IdentityProviderTemplateInterface + IdentityProviderTemplateInterface, + IdentityProviderTemplateItemInterface } from "../../../../../identity-providers/models/identity-provider"; import { IdentityProviderManagementUtils } from "../../../../../identity-providers/utils/identity-provider-management-utils"; +import { + IdentityProviderTemplateManagementUtils +} from "../../../../../identity-providers/utils/identity-provider-template-management-utils"; import { OrganizationType } from "../../../../../organizations/constants"; import { getGeneralIcons } from "../../../../configs/ui"; import { AuthenticationStepInterface } from "../../../../models"; @@ -77,7 +88,7 @@ import { AuthenticationStepInterface } from "../../../../models"; /** * Prop-types for the Add authenticator modal component. */ -interface AddAuthenticatorModalPropsInterface extends TestableComponentInterface, ModalProps { +export interface AddAuthenticatorModalPropsInterface extends TestableComponentInterface, ModalProps { /** * Allow social login addition. */ @@ -85,23 +96,21 @@ interface AddAuthenticatorModalPropsInterface extends TestableComponentInterface /** * Set of authenticators. */ - authenticators: GenericAuthenticatorInterface[]; + authenticators: { + local: GenericAuthenticatorInterface[]; + social: GenericAuthenticatorInterface[]; + enterprise: GenericAuthenticatorInterface[]; + secondFactor: GenericAuthenticatorInterface[]; + recovery: GenericAuthenticatorInterface[]; + }; /** * Configured authentication steps. */ authenticationSteps: AuthenticationStepInterface[]; - /** - * Categorized IDP templates. - */ - categorizedIDPTemplates: IdentityProviderTemplateCategoryInterface[]; /** * Current step. */ currentStep: number; - /** - * Callback to be triggered when add new button is clicked. - */ - onAddNewClick: () => void; /** * Callback to trigger IDP create wizard. */ @@ -126,9 +135,6 @@ interface AddAuthenticatorModalPropsInterface extends TestableComponentInterface * Show/Hide authenticator labels in UI. */ showLabels?: boolean; - subjectStepId: number; - attributeStepId: number; - refreshAuthenticators: () => Promise; } /** @@ -149,34 +155,40 @@ export const AddAuthenticatorModal: FunctionComponent state.config?.ui?.hiddenAuthenticators); + const groupedIDPTemplates: IdentityProviderTemplateItemInterface[] = useSelector( + (state: AppState) => state.identityProvider?.groupedTemplates + ); + const orgType: OrganizationType = useSelector((state: AppState) => { + return state?.organization?.organizationType; + }); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + const [ isModalOpen ] = useState(open); const [ selectedAuthenticators, setSelectedAuthenticators ] = useState([]); + const [ allAuthenticators, setAllAuthenticators ] = useState([]); const [ filteredAuthenticators, setFilteredAuthenticators @@ -186,27 +198,51 @@ export const AddAuthenticatorModal: FunctionComponent(false); const [ filterLabels, setFilterLabels ] = useState([]); const [ selectedFilterLabels, setSelectedFilterLabels ] = useState([]); + const [ + categorizedIdPTemplates, + setCategorizedIdPTemplates + ] = useState([]); - const classes: string = classNames( - "add-authenticator-modal", - className - ); + /** + * Fetches IdP templates if not available. + */ + useEffect(() => { + if (groupedIDPTemplates) { + return; + } - const eventPublisher: EventPublisher = EventPublisher.getInstance(); - const orgType: OrganizationType = useSelector((state: AppState) => - state?.organization?.organizationType); + IdentityProviderTemplateManagementUtils.getIdentityProviderTemplates(); + }, [ groupedIDPTemplates ]); /** * Update the internal filtered authenticators state when the prop changes. */ useEffect(() => { - if (!authenticators) { + if (!unfilteredAuthenticators) { return; } + let _filteredAuthenticators: GenericAuthenticatorInterface[] = [ + ...moderateAuthenticators(unfilteredAuthenticators.local, + AuthenticatorCategories.LOCAL, + t(AuthenticatorMeta.getAuthenticatorTypeDisplayName(AuthenticatorCategories.LOCAL))), + ...moderateAuthenticators(unfilteredAuthenticators.social, + AuthenticatorCategories.SOCIAL, + t(AuthenticatorMeta.getAuthenticatorTypeDisplayName(AuthenticatorCategories.SOCIAL))), + ...moderateAuthenticators(unfilteredAuthenticators.secondFactor, + AuthenticatorCategories.SECOND_FACTOR, + t(AuthenticatorMeta.getAuthenticatorTypeDisplayName(AuthenticatorCategories.SECOND_FACTOR))), + ...moderateAuthenticators(unfilteredAuthenticators.enterprise, + AuthenticatorCategories.ENTERPRISE, + t(AuthenticatorMeta.getAuthenticatorTypeDisplayName(AuthenticatorCategories.ENTERPRISE))), + ...moderateAuthenticators(unfilteredAuthenticators.recovery, + AuthenticatorCategories.RECOVERY, + t(AuthenticatorMeta.getAuthenticatorTypeDisplayName(AuthenticatorCategories.RECOVERY))) + ]; + // Remove SMS OTP authenticator from the list at the sub org level. - const filteredAuthenticators: GenericAuthenticatorInterface[] = (orgType === OrganizationType.SUBORGANIZATION) - ? authenticators.filter((authenticator: GenericAuthenticatorInterface) => { + _filteredAuthenticators = (orgType === OrganizationType.SUBORGANIZATION) + ? _filteredAuthenticators.filter((authenticator: GenericAuthenticatorInterface) => { return ( authenticator.name !== IdentityProviderManagementConstants.SMS_OTP_AUTHENTICATOR_ID && @@ -214,11 +250,85 @@ export const AddAuthenticatorModal: FunctionComponent => { + + return IdentityProviderTemplateManagementUtils.categorizeTemplates(templates) + .then((response: IdentityProviderTemplateCategoryInterface[]) => { + + let tags: string[] = []; + + response.filter((category: IdentityProviderTemplateCategoryInterface) => { + // Order the templates by pushing coming soon items to the end. + category.templates = orderBy(category.templates, [ "comingSoon" ], [ "desc" ]); + + category.templates.filter((template: IdentityProviderTemplateInterface) => { + if (!(template?.tags && Array.isArray(template.tags) && template.tags.length > 0)) { + return; + } + + tags = union(tags, template.tags); + }); + }); + + setCategorizedIdPTemplates(response); + }) + .catch(() => { + setCategorizedIdPTemplates([]); + }); + }; + + /** + * Filter out the displayable set of authenticators by validating against + * the array of authenticators defined to be hidden in the config. + * + * @param authenticators - Authenticators to be filtered. + * @param category - Authenticator category. + * @param categoryDisplayName - Authenticator category display name. + * @returns List of moderated authenticators + */ + const moderateAuthenticators = (authenticators: GenericAuthenticatorInterface[], + category: string, + categoryDisplayName: string) => { + + if (isEmpty(authenticators)) { + return []; + } + + // If the config is undefined or empty, return the original. + if (!hiddenAuthenticators + || !Array.isArray(hiddenAuthenticators) + || hiddenAuthenticators.length < 1) { + + return authenticators; + } + + return authenticators + .filter((authenticator: GenericAuthenticatorInterface) => { + return !hiddenAuthenticators.includes(authenticator.name); + }) + .map((authenticator: GenericAuthenticatorInterface) => { + return { + ...authenticator, + category, + categoryDisplayName + }; + }); + }; /** * Extract Authenticator labels. @@ -324,7 +434,7 @@ export const AddAuthenticatorModal: FunctionComponent filterLabels.includes(selectedLabel)); }; - return authenticators.filter((authenticator: GenericAuthenticatorInterface) => { + return allAuthenticators.filter((authenticator: GenericAuthenticatorInterface) => { if (!query) { return isFiltersMatched(authenticator); @@ -389,7 +499,7 @@ export const AddAuthenticatorModal: FunctionComponent