From a08fae375ea5d190cb4ec06489d7aedd564159a7 Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 12 Dec 2024 22:35:00 +0530 Subject: [PATCH 01/13] Group and display the grouped tile in new connection page --- .../api/use-get-connection-templates.ts | 2 ++ .../admin.connections.v1/configs/templates.ts | 6 ++++++ .../common-authenticator-constants.ts | 6 ++++++ .../constants/connection-ui-constants.ts | 2 ++ .../groups/custom-authentication.json | 15 ++++++++++++++ .../utils/connection-template-utils.ts | 20 +++++++++++++++++++ 6 files changed, 51 insertions(+) create mode 100644 features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json diff --git a/features/admin.connections.v1/api/use-get-connection-templates.ts b/features/admin.connections.v1/api/use-get-connection-templates.ts index 8965c6ce614..07c7bc1ab0e 100644 --- a/features/admin.connections.v1/api/use-get-connection-templates.ts +++ b/features/admin.connections.v1/api/use-get-connection-templates.ts @@ -80,6 +80,7 @@ export const useGetConnectionTemplates = { + // TODO: update the display order return a.displayOrder - b.displayOrder; }); } diff --git a/features/admin.connections.v1/configs/templates.ts b/features/admin.connections.v1/configs/templates.ts index f1b4532bdc5..164873c4ef2 100644 --- a/features/admin.connections.v1/configs/templates.ts +++ b/features/admin.connections.v1/configs/templates.ts @@ -19,6 +19,7 @@ import keyBy from "lodash-es/keyBy"; import values from "lodash-es/values"; import DefaultConnectionTemplateCategory from "../meta/templates-meta/categories/default.json"; +import CustomAuthenticationTemplateGroup from "../meta/templates-meta/groups/custom-authentication.json"; import EnterpriseConnetionTemplateGroup from "../meta/templates-meta/groups/enterprise.json"; import { ConnectionTemplatesConfigInterface } from "../models/connection"; @@ -44,6 +45,11 @@ export const getConnectionTemplatesConfig = (): ConnectionTemplatesConfigInterfa enabled: EnterpriseConnetionTemplateGroup.enabled, id: EnterpriseConnetionTemplateGroup.id, resource: EnterpriseConnetionTemplateGroup + }, + { + enabled: CustomAuthenticationTemplateGroup.enabled, + id: CustomAuthenticationTemplateGroup.id, + resource: CustomAuthenticationTemplateGroup } ], "id" diff --git a/features/admin.connections.v1/constants/common-authenticator-constants.ts b/features/admin.connections.v1/constants/common-authenticator-constants.ts index c8d7f7810e8..65ee13a88e5 100644 --- a/features/admin.connections.v1/constants/common-authenticator-constants.ts +++ b/features/admin.connections.v1/constants/common-authenticator-constants.ts @@ -31,12 +31,15 @@ export class CommonAuthenticatorConstants { */ public static readonly CONNECTION_TEMPLATE_IDS: { APPLE: string; + CUSTOM_AUTHENTICATION: string; ENTERPRISE: string; EXPERT_MODE: string; + EXTERNAL_CUSTOM_AUTHENTICATION: string; FACEBOOK: string; GITHUB: string; GOOGLE: string; HYPR: string; + INTERNAL_CUSTOM_AUTHENTICATION: string; IPROOV: string; LINKEDIN: string; MICROSOFT: string; @@ -47,12 +50,15 @@ export class CommonAuthenticatorConstants { TRUSTED_TOKEN_ISSUER: string; } = { APPLE: "apple-idp", + CUSTOM_AUTHENTICATION: "custom-authentication", ENTERPRISE: "enterprise-idp", EXPERT_MODE: "expert-mode-idp", + EXTERNAL_CUSTOM_AUTHENTICATION: "external-custom-authentication", FACEBOOK: "facebook-idp", GITHUB: "github-idp", GOOGLE: "google-idp", HYPR: "hypr-idp", + INTERNAL_CUSTOM_AUTHENTICATION: "internal-custom-authentication", IPROOV: "iproov-idp", LINKEDIN: "linkedin-idp", MICROSOFT: "microsoft-idp", diff --git a/features/admin.connections.v1/constants/connection-ui-constants.ts b/features/admin.connections.v1/constants/connection-ui-constants.ts index 214610bbfba..7db8ff5308d 100644 --- a/features/admin.connections.v1/constants/connection-ui-constants.ts +++ b/features/admin.connections.v1/constants/connection-ui-constants.ts @@ -313,8 +313,10 @@ export class ConnectionUIConstants { * Set of connection template group Ids. */ public static readonly CONNECTION_TEMPLATE_GROUPS: { + CUSTOM_AUTHENTICATION: string; ENTERPRISE_PROTOCOLS: string; } = { + CUSTOM_AUTHENTICATION: "custom-authentication", ENTERPRISE_PROTOCOLS: "enterprise-protocols" }; diff --git a/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json new file mode 100644 index 00000000000..96fc9fa91b1 --- /dev/null +++ b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json @@ -0,0 +1,15 @@ +{ + "category": "DEFAULT", + "description": "Enable login for users with external authentication service.", + "enabled": true, + "displayOrder": 7, + "id": "custom-authentication", + "docLink": "", + "image": "assets/images/logos/expert.svg", + "name": "Custom Authentication", + "services": [], + "disabled": false, + "type": "CUSTOM", + "tags": [ "CUSTOM" ], + "templateId": "custom-authentication" +} diff --git a/features/admin.connections.v1/utils/connection-template-utils.ts b/features/admin.connections.v1/utils/connection-template-utils.ts index a2099fa9c17..a33b869c6ca 100644 --- a/features/admin.connections.v1/utils/connection-template-utils.ts +++ b/features/admin.connections.v1/utils/connection-template-utils.ts @@ -281,6 +281,26 @@ export const groupConnectionTemplates = ( }); } + /** + * Custom authenticators are grouped under "Custom Authentication". + */ + if (group.id === ConnectionUIConstants.CONNECTION_TEMPLATE_GROUPS.CUSTOM_AUTHENTICATION) { + const subTemplateIds: string[] = [ + CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXTERNAL_CUSTOM_AUTHENTICATION, + CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.INTERNAL_CUSTOM_AUTHENTICATION + ]; + + updatedGroup.subTemplates = _templates + .filter((template: ConnectionTemplateInterface) => { + return subTemplateIds.includes(template.id); + }); + + // Remove grouped sub templates from main template list. + _templates = _templates.filter((template: ConnectionTemplateInterface) => { + return !subTemplateIds.includes(template.id); + }); + } + groupedTemplates.push(updatedGroup); } From 3bc45dafbc5c408a8e4db65c750eb9c3d314fb96 Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 2 Jan 2025 12:20:11 +0530 Subject: [PATCH 02/13] Add 2FA constants --- .../constants/common-authenticator-constants.ts | 4 +++- .../admin.connections.v1/utils/connection-template-utils.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/features/admin.connections.v1/constants/common-authenticator-constants.ts b/features/admin.connections.v1/constants/common-authenticator-constants.ts index 65ee13a88e5..028e56685d5 100644 --- a/features/admin.connections.v1/constants/common-authenticator-constants.ts +++ b/features/admin.connections.v1/constants/common-authenticator-constants.ts @@ -48,6 +48,7 @@ export class CommonAuthenticatorConstants { SAML: string; SWE: string; TRUSTED_TOKEN_ISSUER: string; + TWO_FACTOR_CUSTOM_AUTHENTICATION: string; } = { APPLE: "apple-idp", CUSTOM_AUTHENTICATION: "custom-authentication", @@ -66,7 +67,8 @@ export class CommonAuthenticatorConstants { ORGANIZATION_ENTERPRISE_IDP: "organization-enterprise-idp", SAML: "enterprise-saml-idp", SWE: "swe-idp", - TRUSTED_TOKEN_ISSUER: "trusted-token-issuer" + TRUSTED_TOKEN_ISSUER: "trusted-token-issuer", + TWO_FACTOR_CUSTOM_AUTHENTICATION: "two-factor-custom-authentication" }; /** diff --git a/features/admin.connections.v1/utils/connection-template-utils.ts b/features/admin.connections.v1/utils/connection-template-utils.ts index a33b869c6ca..8d4657ca2b7 100644 --- a/features/admin.connections.v1/utils/connection-template-utils.ts +++ b/features/admin.connections.v1/utils/connection-template-utils.ts @@ -287,7 +287,8 @@ export const groupConnectionTemplates = ( if (group.id === ConnectionUIConstants.CONNECTION_TEMPLATE_GROUPS.CUSTOM_AUTHENTICATION) { const subTemplateIds: string[] = [ CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXTERNAL_CUSTOM_AUTHENTICATION, - CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.INTERNAL_CUSTOM_AUTHENTICATION + CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.INTERNAL_CUSTOM_AUTHENTICATION, + CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.TWO_FACTOR_CUSTOM_AUTHENTICATION ]; updatedGroup.subTemplates = _templates From 5141b33ed37b36deda2b49042f102f14f1a34b32 Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 2 Jan 2025 15:31:38 +0530 Subject: [PATCH 03/13] Update the display order of custom authentication --- .../api/use-get-connection-templates.ts | 1 - .../meta/templates-meta/groups/custom-authentication.json | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/features/admin.connections.v1/api/use-get-connection-templates.ts b/features/admin.connections.v1/api/use-get-connection-templates.ts index 07c7bc1ab0e..e1e0d5afbdc 100644 --- a/features/admin.connections.v1/api/use-get-connection-templates.ts +++ b/features/admin.connections.v1/api/use-get-connection-templates.ts @@ -103,7 +103,6 @@ export const useGetConnectionTemplates = { - // TODO: update the display order return a.displayOrder - b.displayOrder; }); } diff --git a/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json index 96fc9fa91b1..71036a5bc66 100644 --- a/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json +++ b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json @@ -2,14 +2,14 @@ "category": "DEFAULT", "description": "Enable login for users with external authentication service.", "enabled": true, - "displayOrder": 7, + "displayOrder": 12, "id": "custom-authentication", "docLink": "", "image": "assets/images/logos/expert.svg", "name": "Custom Authentication", "services": [], "disabled": false, - "type": "CUSTOM", - "tags": [ "CUSTOM" ], + "type": "DEFAULT", + "tags": [ "Custom" ], "templateId": "custom-authentication" } From 5550239cf9ffe7ee34bcfee807333f403d662c54 Mon Sep 17 00:00:00 2001 From: Shenali Date: Tue, 7 Jan 2025 21:33:54 +0530 Subject: [PATCH 04/13] Add initial custom auth create wizard --- .../custom-authentication-create-wizard.tsx | 1676 +++++++++++++++++ 1 file changed, 1676 insertions(+) create mode 100644 features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx new file mode 100644 index 00000000000..734199247e1 --- /dev/null +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -0,0 +1,1676 @@ +/** + * Copyright (c) 2023-2024, 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 + * 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 { SelectChangeEvent } from "@mui/material"; +import Backdrop from "@mui/material/Backdrop"; +import Alert from "@oxygen-ui/react/Alert"; +import AlertTitle from "@oxygen-ui/react/AlertTitle"; +import Box from "@oxygen-ui/react/Box"; +import Button from "@oxygen-ui/react/Button"; +import Divider from "@oxygen-ui/react/Divider"; +import Grid from "@oxygen-ui/react/Grid"; +import InputAdornment from "@oxygen-ui/react/InputAdornment"; +import Skeleton from "@oxygen-ui/react/Skeleton"; +import { FeatureAccessConfigInterface, useRequiredScopes } from "@wso2is/access-control"; +import { AppState, EventPublisher } from "@wso2is/admin.core.v1"; +import { ModalWithSidePanel } from "@wso2is/admin.core.v1/components"; +import { IdentityAppsError } from "@wso2is/core/errors"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { URLUtils } from "@wso2is/core/utils"; +import { + Field, + FinalForm, + FinalFormField, + FormRenderProps, + FormSpy, + SelectFieldAdapter, + TextFieldAdapter, + Wizard2, + WizardPage +} from "@wso2is/form"; +import { + ContentLoader, + DocumentationLink, + EmphasizedSegment, + GenericIcon, + Heading, + Hint, + LinkButton, + PrimaryButton, + SelectionCard, + Steps, + useDocumentation, + useWizardAlert +} from "@wso2is/react-components"; +import { FormValidation } from "@wso2is/validation"; +import { AxiosError, AxiosResponse } from "axios"; +import cloneDeep from "lodash-es/cloneDeep"; +import isEmpty from "lodash-es/isEmpty"; +import kebabCase from "lodash-es/kebabCase"; +import React, { + FC, + MutableRefObject, + PropsWithChildren, + ReactElement, + ReactNode, + Suspense, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { Dispatch } from "redux"; +import { Icon, Grid as SemanticGrid } from "semantic-ui-react"; +import { createConnection, useGetConnectionTemplate } from "../../api/connections"; +import { getConnectionIcons, getConnectionWizardStepIcons } from "../../configs/ui"; +import { ConnectionUIConstants } from "../../constants/connection-ui-constants"; +import { LocalAuthenticatorConstants } from "../../constants/local-authenticator-constants"; +import { + AuthenticationType, + AuthenticationTypeDropdownOption, + AuthenticationPropertiesInterface, + ConnectionInterface, + ConnectionTemplateInterface, + CustomAuthenticationCreateWizardGeneralFormValuesInterface, + EndpointInterface, + EndpointConfigFormPropertyInterface, + GenericConnectionCreateWizardPropsInterface, +} from "../../models/connection"; + +/** + * Proptypes for the custom authenticator + * creation wizard component. + */ +interface CustomAuthenticationCreateWizardProps extends + GenericConnectionCreateWizardPropsInterface, IdentifiableComponentInterface { +} + +/** + * Constants for wizard steps. As per the requirement we have three + * wizard steps. Here are the step definitions:- + * + * AUTHENTICATION_TYPE - The authentication type of the custom authenticator. + * GENERAL_SETTINGS - Contains general details common to all the authenticators. + * CONFIGURATION - Includes the external endpoint configuration details. + */ +enum WizardSteps { + AUTHENTICATION_TYPE = "Authentication Type", + GENERAL_SETTINGS = "General Settings", + CONFIGURATION = "Configuration" +} + +interface WizardStepInterface { + icon: any; + title: string; + submitCallback: any; + name: WizardSteps; +} + +/** + * Prop types for the endpoint configuration form component. + */ +interface EndpointConfigFormInterface extends IdentifiableComponentInterface { + /** + * Endpoint's initial values. + */ + initialValues: EndpointConfigFormInterface; + /** + * Flag for loading state. + */ + isLoading?: boolean; + /** + * Specifies action creation state. + */ + isCreateFormState: boolean; +} + +type AvailableCustomAuthentications = "external" | "internal" | "two-factor"; +type MinMax = { min: number; max: number }; +type FormErrors = { [ key: string ]: string }; + +export const CustomAuthenticationCreateWizard: FC = ( + props: PropsWithChildren +): ReactElement => { + + const { + onWizardClose, + onIDPCreate, + title, + subTitle, + template, + [ "data-componentid" ]: componentId + } = props; + + const wizardRef: MutableRefObject = useRef(null); + + const [ initWizard, setInitWizard ] = useState(false); + const [ wizardSteps, setWizardSteps ] = useState([]); + const [ currentWizardStep, setCurrentWizardStep ] = useState(0); + const [ alert, setAlert, alertComponent ] = useWizardAlert(); + const [ selectedAuthenticator, setSelectedAuthenticator ] = useState("external"); + const [ selectedTemplateId, setSelectedTemplateId ] = useState(null); + const [ isSubmitting, setIsSubmitting ] = useState(false); + const [ isShowSecret1, setIsShowSecret1 ] = useState(false); + const [ isShowSecret2, setIsShowSecret2 ] = useState(false); + const [ isAuthenticationCreateState, setIsAuthenticationCreateState ] = useState(true); + const [ isAuthenticationUpdateState, setIsAuthenticationUpdateState ] = useState(false); + const [ isLoading, setIsLoading ] = useState(false); + const [ authenticationType, setAuthenticationType ] = useState(null); + const [ selectedAuthenticationType, setSelectedAuthenticationType ] = useState(); + // const [ selectedEndpointAuthType, setSelectedEndpointAuthType ] = useState(); + + // Dynamic UI state + const [ nextShouldBeDisabled, setNextShouldBeDisabled ] = useState(true); + + const dispatch: Dispatch = useDispatch(); + const { t } = useTranslation(); + const { getLink } = useDocumentation(); + + const endpointFeatureConfig: FeatureAccessConfigInterface = useSelector( + (state: AppState) => state.config.ui.features.actions); + const hasActionUpdatePermissions: boolean = useRequiredScopes(endpointFeatureConfig?.scopes?.update); + const hasActionCreatePermissions: boolean = useRequiredScopes(endpointFeatureConfig?.scopes?.create); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + const { + data: connectionTemplate, + isLoading: isConnectionTemplateFetchRequestLoading + } = useGetConnectionTemplate(selectedTemplateId, selectedTemplateId !== null); + + useEffect(() => { + if (!initWizard) { + setWizardSteps(getWizardSteps()); + setInitWizard(true); + } + }, [ initWizard ]); + + useEffect(() => { + + const templateId: string = selectedAuthenticator === "external" + ? "external-user-authentication" + : selectedAuthenticator === "internal" + ? "internal-user-authentication" + : "enterprise-oidc-idp"; + + setSelectedTemplateId(templateId); + }, [ selectedAuthenticator ]); + + // useEffect(() => { + // if(selectedAuthenticationType) { + // setSelectedValue(selectedSubjectValue); + // setShowSubjectAttribute(selectedSubjectValue !== defaultSubjectAttribute); + // } + // }, [ selectedSubjectValue ]); + + const initialValues: { NameIDType: string, RequestMethod: string, + identifier: string, displayName: string } = useMemo(() => ({ + NameIDType: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + RequestMethod: "post", + displayName: EMPTY_STRING, + identifier: EMPTY_STRING + }), []); + + // TODO: [Immediate] use the API to get initial endpoint details + // const { + // data: action, + // error: actionFetchRequestError, + // isLoading: isActionLoading, + // mutate: mutateAction + // } = useGetActionById(actionTypeApiPath, actionId); + + // const endpointInitialValues: EndpointConfigFormInterface = useMemo(() => { + // return { + // authenticationType: action?.endpoint?.authentication?.type.toString(), + // endpointUri: action?.endpoint?.uri, + // id: action?.id, + // name: action?.name + // }; + // }, [action]); // TODO: add dep + + const endpointInitialValues: EndpointConfigFormPropertyInterface = useMemo(() => { + return { + authenticationType: "", + endpointUri: "", + id: "" + }; + }, []); + + /** + * The following useEffect is used to set the current Action Authentication Type. + */ + useEffect(() => { + if (!initialValues?.identifier) { + setIsAuthenticationCreateState(true); + } else { + // setAuthenticationType(endpointInitialValues.authenticationType as AuthenticationType); + setIsAuthenticationUpdateState(false); + } + }, [ initialValues ]); + + const renderInputAdornmentOfSecret = (showSecret: boolean, onClick: () => void): ReactElement => ( + + + + ); + + const getWizardSteps: () => WizardStepInterface[] = () => { + return [ + { + icon: getConnectionWizardStepIcons().general, + name: WizardSteps.AUTHENTICATION_TYPE, + title: "Authentication Type" + }, + { + icon: getConnectionWizardStepIcons().authenticatorSettings, + name: WizardSteps.GENERAL_SETTINGS, + title: "General Settings" + }, + { + icon: getConnectionWizardStepIcons().general, + name: WizardSteps.CONFIGURATION, + title: "Configuration" + } + ] as WizardStepInterface[]; + }; + + const renderDimmerOverlay = (): ReactNode => { + return ( + + { t("common:featureAvailable") } + + ); + }; + + // TODO: update this method + /** + * @param values - form values + * @param form - form instance + * @param callback - callback to proceed to the next step + */ + const handleFormSubmit = (values: any) => { + + const { customAuth: customAuthenticator } = cloneDeep(connectionTemplate); + + customAuthenticator.templateId = selectedTemplateId; + + // THIS HAS THE FINAL FIELD VALUES SUBMITTED + // Populate user entered values + customAuthenticator.name = values?.identifier?.toString(); + customAuthenticator.displayName = values?.displayName?.toString(); + console.log("name: " + customAuthenticator.name); + console.log("displayName: " + customAuthenticator.displayName); + // TODO: [Immediate] add endpoint details here + // customAuthenticator.endpoint.uri = values?.uri?.toString(); + + // TODO: update the image + customAuthenticator.image = "assets/images/logos/expert.svg"; + + setIsSubmitting(true); + + // TODO: [Immediate] API integrations + createConnection(customAuthenticator) + .then((response: AxiosResponse) => { + eventPublisher.publish("connections-finish-adding-connection", { + type: componentId + "-" + kebabCase(selectedAuthenticator) + }); + dispatch(addAlert({ + description: t("authenticationProvider:notifications." + + "addIDP.success.description"), + level: AlertLevels.SUCCESS, + message: t("authenticationProvider:notifications." + + "addIDP.success.message") + })); + // The created resource's id is sent as a location header. + // If that's available, navigate to the edit page. + if (!isEmpty(response.headers.location)) { + const location: string = response.headers.location; + const createdIdpID: string = location.substring(location.lastIndexOf("/") + 1); + + onIDPCreate(createdIdpID); + + return; + } + onIDPCreate(); + }) + .catch((error: AxiosError) => { + const identityAppsError: IdentityAppsError = ConnectionUIConstants.ERROR_CREATE_LIMIT_REACHED; + + if (error.response.status === 403 && + error?.response?.data?.code === + identityAppsError.getErrorCode()) { + + setAlert({ + code: identityAppsError.getErrorCode(), + description: t( + identityAppsError.getErrorDescription() + ), + level: AlertLevels.ERROR, + message: t( + identityAppsError.getErrorMessage() + ), + traceId: identityAppsError.getErrorTraceId() + }); + setTimeout(() => setAlert(undefined), 4000); + + return; + } + + if (error?.response.status === 500 && + error.response?.data.code === "IDP-65002") { + setAlert({ + description: "You are trying to add a provider with an existing Identity" + // TODO: update this + " Provider Entity ID or a Service Provider Entity ID.", + level: AlertLevels.ERROR, + message: "There's a Conflicting Entity" + }); + setTimeout(() => setAlert(undefined), 8000); + + return; + } + + if (error.response && error.response.data && error.response.data.description) { + setAlert({ + description: t("authenticationProvider:notifications." + + "addIDP.error.description", + { description: error.response.data.description }), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications." + + "addIDP.error.message") + }); + setTimeout(() => setAlert(undefined), 4000); + + return; + } + setAlert({ + description: t("authenticationProvider:notifications." + + "addIDP.genericError.description"), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications." + + "addIDP.genericError.message") + }); + setTimeout(() => setAlert(undefined), 4000); + }) + .finally(() => { + setIsSubmitting(false); + }); + + }; + + useEffect (() => { + console.log("Auth type", selectedAuthenticationType); + }, [ selectedAuthenticationType ]); + + const handleDropdownChange = (event, data) => { + setSelectedAuthenticationType(data.value); // Update the state with the selected value + console.log("Selected value:", data.value); // Log the selected value for debugging + }; + + const validateForm = (values: EndpointConfigFormPropertyInterface): + Partial => { const error: Partial = {}; + + console.log("value type: " + values?.authenticationType); + console.log("value uri: " + values?.endpointUri); + // TODO: use local - and update with proper local + if (!values?.endpointUri) { + error.endpointUri = "Empty endpoint URI"; + } + if (URLUtils.isURLValid(values?.endpointUri)) { + if (!(URLUtils.isHttpsUrl(values?.endpointUri))) { + error.endpointUri = t("actions:fields.endpoint.validations.notHttps"); + } + } else { + error.endpointUri = t("actions:fields.endpoint.validations.invalidUrl"); + } + + if (!selectedAuthenticationType) { + error.authenticationType = t("actions:fields.authenticationType.validations.empty"); + } + + const apiKeyHeaderRegex: RegExp = /^[a-zA-Z0-9][a-zA-Z0-9-.]+$/; + + switch (authenticationType) { + case AuthenticationType.BASIC: + if (isAuthenticationCreateState || isAuthenticationUpdateState || + values?.usernameAuthProperty || values?.passwordAuthProperty) { + if (!values?.usernameAuthProperty) { + error.usernameAuthProperty = t("actions:fields.authentication." + + "types.basic.properties.username.validations.empty"); + } + if (!values?.passwordAuthProperty) { + error.passwordAuthProperty = t("actions:fields.authentication." + + "types.basic.properties.password.validations.empty"); + } + } + + break; + case AuthenticationType.BEARER: + if (isAuthenticationCreateState || isAuthenticationUpdateState) { + if (!values?.accessTokenAuthProperty) { + error.accessTokenAuthProperty = t("actions:fields.authentication." + + "types.bearer.properties.accessToken.validations.empty"); + } + } + + break; + case AuthenticationType.API_KEY: + if (isAuthenticationCreateState || isAuthenticationUpdateState|| + values?.headerAuthProperty || values?.valueAuthProperty) { + if (!values?.headerAuthProperty) { + error.headerAuthProperty = t("actions:fields.authentication." + + "types.apiKey.properties.header.validations.empty"); + } + if (!apiKeyHeaderRegex.test(values?.headerAuthProperty)) { + error.headerAuthProperty = t("actions:fields.authentication." + + "types.apiKey.properties.header.validations.invalid"); + } + if (!values?.valueAuthProperty) { + error.valueAuthProperty = t("actions:fields.authentication." + + "types.apiKey.properties.value.validations.empty"); + } + } + + break; + default: + break; + } + + return error; + }; + + const handleSubmit = ( + values: EndpointConfigFormPropertyInterface, + changedFields: EndpointConfigFormPropertyInterface) => + { + const authProperties: Partial = {}; + + if (isAuthenticationCreateState || isAuthenticationUpdateState) { + switch (authenticationType) { + case AuthenticationType.BASIC: + authProperties.username = values.usernameAuthProperty; + authProperties.password = values.passwordAuthProperty; + + break; + case AuthenticationType.BEARER: + authProperties.accessToken = values.accessTokenAuthProperty; + + break; + case AuthenticationType.API_KEY: + authProperties.header = values.headerAuthProperty; + authProperties.value = values.valueAuthProperty; + + break; + case AuthenticationType.NONE: + break; + default: + break; + } + } + + if (isAuthenticationCreateState) { + const endpoint: EndpointInterface ={ + authentication: { + properties: authProperties, + type: authenticationType + }, + uri: values.endpointUri + }; + + setIsSubmitting(true); + // TODO: [ Immediate ] add creation API + + // createAction(actionTypeApiPath, actionValues) + // .then(() => { + // handleSuccess(ActionsConstants.CREATE); + // mutateActions(); + // }) + // .catch((error: AxiosError) => { + // handleError(error, ActionsConstants.CREATE); + // }) + // .finally(() => { + // setIsSubmitting(false); + // }); + } else { + // Update endpoint details + const endpoint: EndpointInterface = { + authentication: isAuthenticationUpdateState ? { + properties: authProperties, + type: authenticationType + } : undefined, + uri: changedFields?.endpointUri ? values.endpointUri : undefined + }; + + setIsSubmitting(true); + // TODO: [ Immediate ] add creation API + + // updateAction(actionTypeApiPath, initialValues.id, updatingValues) + // .then(() => { + // handleSuccess(ActionsConstants.UPDATE); + // setIsAuthenticationUpdateFormState(false); + // mutateAction(); + // }) + // .catch((error: AxiosError) => { + // handleError(error, ActionsConstants.UPDATE); + // }) + // .finally(() => { + // setIsSubmitting(false); + // }); + } + }; + + /** + * This is called when the Change Authentication button is pressed. + */ + const handleAuthenticationChange = (): void => { + setIsAuthenticationUpdateState(true); + }; + + /** + * This is called when the cancel button is pressed. + */ + const handleAuthenticationChangeCancel = (): void => { + setAuthenticationType(endpointInitialValues?.authenticationType as AuthenticationType); + setIsAuthenticationUpdateState(false); + }; + + + const getFieldDisabledStatus = (): boolean => { + if (isAuthenticationCreateState) { + return !hasActionCreatePermissions; + } else { + return !hasActionUpdatePermissions; + } + }; + + const renderLoadingPlaceholders = (): ReactElement => ( + + + + + + + ); + + const renderEndpointAuthPropertyFields = (): ReactElement => { + // const showAuthSecretsHint = (): ReactElement => ( + // + // { + // isAuthenticationCreateState ? + // t("actions:fields.authenticationType.hint.create") + // : t("actions:fields.authenticationType.hint.update") + // } + // + // ); + + switch (selectedAuthenticationType) { + case AuthenticationType.NONE: + break; + case AuthenticationType.BASIC: // TODO: [Immediate] check inputType + return ( + <> + {/* { showAuthSecretsHint() } */} + setIsShowSecret1(!isShowSecret1)) + } } + required={ true } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-authentication-property-username` } + width={ 15 } + /> + setIsShowSecret2(!isShowSecret2)) + } } + required={ true } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-authentication-property-password` } + width={ 15 } + /> + + ); + case AuthenticationType.BEARER: + return ( + <> + {/* { showAuthSecretsHint() } */} + setIsShowSecret1(!isShowSecret1)) + } } + label={ "Access token" } + placeholder={ "Access Token" } + required={ true } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-authentication-property-accessToken` } + width={ 15 } + /> + + ); + case AuthenticationType.API_KEY: + return ( + <> + {/* { showAuthSecretsHint() } */} + + Hint + + ) } + required={ true } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-authentication-property-header` } + width={ 15 } + /> + setIsShowSecret2(!isShowSecret2)) + } } + label={ "Value" } + placeholder={ "Value" } + required={ true } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-authentication-property-value` } + width={ 15 } + /> + + ); + default: + break; + } + }; + + // const renderFormFields = (): ReactElement => { + // const renderAuthenticationSection = (): ReactElement => { + // const renderAuthenticationSectionInfoBox = (): ReactElement => { + // const resolveAuthTypeDisplayName = (): string => { + // switch (authenticationType) { + // case AuthenticationType.NONE: // TODO: update with proper local + // return t("actions:fields.authentication.types.none.name"); + // case AuthenticationType.BASIC: + // return t("actions:fields.authentication.types.basic.name"); + // case AuthenticationType.BEARER: + // return t("actions:fields.authentication.types.bearer.name"); + // case AuthenticationType.API_KEY: + // return t("actions:fields.authentication.types.apiKey.name"); + // default: + // return; + // } + // }; + + // return ( + // + // + // } } + // /> + // + // + // If you are changing the authentication, be aware that the authentication secrets of + // the external endpoint need to be updated. + // + //
+ // + //
+ //
+ // ); + // }; + + // const renderAuthenticationUpdateWidget = (): ReactElement => { + // const renderAuthentication = (): ReactElement => { + // const renderAuthenticationPropertyFields = (): ReactElement => { + // const showAuthSecretsHint = (): ReactElement => ( + // + // { + // isAuthenticationCreateState ? + // t("actions:fields.authenticationType.hint.create") + // : t("actions:fields.authenticationType.hint.update") + // } + // + // ); + + // switch (authenticationType) { + // case AuthenticationType.NONE: + // break; + // case AuthenticationType.BASIC: + // return ( + // <> + // { showAuthSecretsHint() } + // setIsShowSecret1(!isShowSecret1)) + // } } + // label={ t("actions:fields.authentication" + + // ".types.basic.properties.username.label") } + // placeholder={ t("actions:fields.authentication" + + // ".types.basic.properties.username.placeholder") } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // setIsShowSecret2(!isShowSecret2)) + // } } + // label={ t("actions:fields.authentication" + + // ".types.basic.properties.password.label") } + // placeholder={ t("actions:fields.authentication" + + // ".types.basic.properties.password.placeholder") } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // + // ); + // case AuthenticationType.BEARER: + // return ( + // <> + // { showAuthSecretsHint() } + // setIsShowSecret1(!isShowSecret1)) + // } } + // label={ t("actions:fields.authentication" + + // ".types.bearer.properties.accessToken.label") } + // placeholder={ t("actions:fields.authentication" + + // ".types.bearer.properties.accessToken.placeholder") } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // + // ); + // case AuthenticationType.API_KEY: + // return ( + // <> + // { showAuthSecretsHint() } + // + // { t("actions:fields.authentication" + + // ".types.apiKey.properties.header.hint") } + // + // ) } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // setIsShowSecret2(!isShowSecret2)) + // } } + // label={ t("actions:fields.authentication" + + // ".types.apiKey.properties.value.label") } + // placeholder={ t("actions:fields.authentication" + + // ".types.apiKey.properties.value.placeholder") } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // + // ); + // default: + // break; + // } + // }; + + // const handleAuthTypeChange = (event: SelectChangeEvent) => { + // switch (event.target.value) { + // case AuthenticationType.NONE.toString(): + // setAuthenticationType(AuthenticationType.NONE); + + // break; + // case AuthenticationType.BASIC.toString(): + // setAuthenticationType(AuthenticationType.BASIC); + + // break; + // case AuthenticationType.BEARER.toString(): + // setAuthenticationType(AuthenticationType.BEARER); + + // break; + // case AuthenticationType.API_KEY.toString(): + // setAuthenticationType(AuthenticationType.API_KEY); + + // break; + // default: + // setAuthenticationType(AuthenticationType.NONE); + // } + + // renderAuthenticationPropertyFields(); + // }; + + // return ( + // <> + // ({ + // text: t(option.text), + // value: option.value.toString() })) + // ] + // } + // onChange={ handleAuthTypeChange } + // disabled={ getFieldDisabledStatus() } + // /> + // {/* { renderAuthenticationPropertyFields() } */} + // + // ); + // }; + + // return ( + // + //
+ // { renderAuthentication() } + // {/* { !isAuthenticationCreateState && ( + // + // ) } */} + //
+ //
+ // ); + // }; + + // // return ( !isAuthenticationUpdateState && !isAuthenticationCreateState && !(authenticationType === null) ? + // // renderAuthenticationSectionInfoBox() : renderAuthenticationUpdateWidget()); + + // return ( renderAuthenticationUpdateWidget()); + // }; + + // if (isLoading) { + // return renderLoadingPlaceholders(); + // } + + // return ( + // <> + // + // { t("actions:fields.endpoint.hint") } + // + // ) } + // component={ TextFieldAdapter } + // maxLength={ 100 } + // minLength={ 0 } + // disabled={ getFieldDisabledStatus() } + // /> + // + // + // { t("actions:fields.authentication.label") } + // + // { renderAuthenticationSection() } + // + // ); + // }; + + const wizardCommonFirstPage = () => ( + { + if (selectedAuthenticator !== null || selectedAuthenticator !== undefined) { + setNextShouldBeDisabled(false); + } + } } + > +
+ + + + External (Federated) User Authentication } + selected={ selectedAuthenticator === "external" } + onClick={ () => setSelectedAuthenticator("external") } + imageSize="x30" + imageOptions={ { + relaxed: true, + square: false, + width: "auto" + } } + contentTopBorder={ false } + showTooltips={ true } + data-componentid={ `${ componentId }-form-wizard-external-custom-authentication- + selection-card` } + /> + Internal User Authentication } + selected={ selectedAuthenticator === "internal" } + onClick={ () => setSelectedAuthenticator("internal") } + imageSize="x30" + imageOptions={ { + relaxed: true, + square: false, + width: "auto" + } } + showTooltips={ true } + disabled={ false } + overlay={ renderDimmerOverlay() } + contentTopBorder={ false } + renderDisabledItemsAsGrayscale={ false } + overlayOpacity={ 0.6 } + data-componentid={ `${ componentId }-form-wizard-internal-custom-authentication- + selection-card` } + /> + 2FA Authentication } + selected={ selectedAuthenticator === "two-factor" } + onClick={ () => setSelectedAuthenticator("two-factor") } + imageSize="x30" + imageOptions={ { + relaxed: true, + square: false, + width: "auto" + } } + showTooltips={ true } + disabled={ false } + overlay={ renderDimmerOverlay() } + contentTopBorder={ false } + renderDisabledItemsAsGrayscale={ false } + overlayOpacity={ 0.6 } + data-componentid={ `${ componentId }-form-wizard-two-factor-custom-authentication- + selection-card` } + /> + + +
+
+ ); + + // TODO: check max and min len of two fields + const generalSettingsPage = () => ( + { + const errors: FormErrors = {}; + + if (!FormValidation.identifier(values.identifier)) { + errors.identifier = "Invalid Identifier"; // TODO: local message + } + if (!FormValidation.isValidResourceName(values.displayName)) { + errors.displayName = "Invalid Display Name"; // TODO: local message + } + + setNextShouldBeDisabled(ifFieldsHave(errors)); + + return errors; + } } + > + + + + ); + + // Final Page + // TODO: check if we need the emphasized segment + const configurationsPage = () => ( + + + + + { "Authentication" } + + + ({ + text: t(option.text), + value: option.value.toString() })) + ] + } + onChange={ handleDropdownChange } + enableReinitialize={ true } + data-componentid={ `${ componentId }-endpoint_authentication-dropdown` } + width={ 15 } + /> +
+ { renderEndpointAuthPropertyFields() } +
+
+
+ ); + + // ### Here the configurationsPage is written with FinalForm + // const configurationsPage = () => ( + // { + // handleSubmit(values, form.getState().dirtyFields); } + // } + // validate={ validateForm } + // // initialValues={ endpointInitialValues } + // render={ ({ handleSubmit, form }: FormRenderProps) => ( + //
+ // + //
+ // { renderFormFields() } + // { !isLoading && ( + // + // ) } + //
+ //
+ // + // { ({ values }: { values: EndpointConfigFormPropertyInterface }) => { + // if (!isAuthenticationUpdateState) { + // form.change("authenticationType", + // endpointInitialValues?.authenticationType); + // switch (authenticationType) { + // case AuthenticationType.BASIC: + // delete values.usernameAuthProperty; + // delete values.passwordAuthProperty; + + // break; + // case AuthenticationType.BEARER: + // delete values.accessTokenAuthProperty; + + // break; + // case AuthenticationType.API_KEY: + // delete values.headerAuthProperty; + // delete values.valueAuthProperty; + + // break; + // default: + // break; + // } + // } + + // // Clear inputs of property field values of other authentication types. + // switch (authenticationType) { + // case AuthenticationType.BASIC: + // delete values.accessTokenAuthProperty; + // delete values.headerAuthProperty; + // delete values.valueAuthProperty; + + // break; + // case AuthenticationType.BEARER: + // delete values.usernameAuthProperty; + // delete values.passwordAuthProperty; + // delete values.headerAuthProperty; + // delete values.valueAuthProperty; + + // break; + // case AuthenticationType.API_KEY: + // delete values.usernameAuthProperty; + // delete values.passwordAuthProperty; + // delete values.accessTokenAuthProperty; + + // break; + // case AuthenticationType.NONE: + // delete values.usernameAuthProperty; + // delete values.passwordAuthProperty; + // delete values.headerAuthProperty; + // delete values.valueAuthProperty; + // delete values.accessTokenAuthProperty; + + // break; + // default: + + // break; + // } + + // return null; + // } } + // + //
+ // ) } + // > + //
+ // ); + + // Resolvers + const resolveWizardPages = (): Array => { + return [ + wizardCommonFirstPage(), + generalSettingsPage(), + configurationsPage() + ]; + }; + + const resolveHelpPanel = () => { + + const SECOND_STEP: number = 1; + + if (currentWizardStep !== SECOND_STEP) return null; + + // Return null when `showHelpPanel` is false or `samlHelp` + // or `oidcHelp` is not defined in `selectedTemplate` object. + + const subTemplate: ConnectionTemplateInterface = cloneDeep(template.subTemplates.find( + ({ id }: { id: string }) => { + return id === (selectedAuthenticator === "external" + ? "external-custom-authentication" + : selectedAuthenticator === "internal" + ? "internal-custom-authentication" + : "two-factor-custom-authentication" + ); + } + )); + + // TODO: update this with correct wizard helps + if (!subTemplate?.content?.wizardHelp) return null; + + // let { wizardHelp: WizardHelp } = subTemplate?.content; + + // if (selectedAuthenticator === "external" && selectedSamlConfigMode === "file") { + // WizardHelp = subTemplate.content.fileBasedHelpPanel; + // } + + return ( + + +
+ Help +
+
+ + }> + {/* */} + + +
+ ); + + }; + + /** + * Resolves the documentation link when a protocol is selected. + * @returns Documetation link. + */ + const resolveDocumentationLink = (): ReactElement => { + let docLink: string = undefined; + + if (selectedAuthenticator === "external") { + docLink = getLink("develop.connections.newConnection.enterprise.samlLearnMore"); + } + + if (selectedAuthenticator === "internal") { + docLink = getLink("develop.connections.newConnection.enterprise.oidcLearnMore"); + } + + if (selectedAuthenticator === "two-factor") { + docLink = getLink("develop.connections.newConnection.enterprise.oidcLearnMore"); + } + + return ( + + { t("common:learnMore") } + + ); + }; + + // Start: Modal + + return ( + + + { /*Modal header*/ } + +
+ +
+ { title } + { subTitle && ( + + { subTitle } + { resolveDocumentationLink() } + + ) } +
+
+
+ { /*Modal body content*/ } + + + + { wizardSteps.map((step: any, index: number) => ( + + )) } + + + + { alert && alertComponent } + setCurrentWizardStep(index) } + data-componentid={ componentId } + > + { resolveWizardPages() } + + + + { /*Modal actions*/ } + + + + + + { t("common:cancel") } + + + + { /*Check whether we have more steps*/ } + { currentWizardStep < wizardSteps.length - 1 && ( + { + wizardRef.current.gotoNextPage(); + } } + data-testid="add-connection-modal-next-button" + > + { t("authenticationProvider:wizards.buttons.next") } + + + ) } + { /*Check whether its the last step*/ } + { currentWizardStep === wizardSteps.length - 1 && ( + // Note that we use the same logic as the next button + // element. This is because we pass a callback to + // onSubmit which triggers a dedicated handler. + { + wizardRef.current.gotoNextPage(); + } } + data-testid="add-connection-modal-finish-button" + loading={ isSubmitting } + > + { t("authenticationProvider:wizards.buttons.finish") } + + ) } + { currentWizardStep > 0 && ( + wizardRef.current.gotoPreviousPage() } + data-testid="add-connection-modal-previous-button" + > + + { t("authenticationProvider:wizards.buttons." + + "previous") } + + ) } + + + + +
+ { resolveHelpPanel() } +
+ ); + +}; + +/** + * Default props for the custom authenticator + * creation wizard. + */ +CustomAuthenticationCreateWizard.defaultProps = { + currentStep: 0, + "data-componentid": "custom-authentication" +}; + + +// General constants +const EMPTY_STRING: string = ""; + +// Validation Functions. +// FIXME: These will be removed in the future when +// form module validation gets to a stable state. + +/** + * Given a {@link FormErrors} object, it will check whether + * every key has a assigned truthy value. {@link Array.every} + * will return true if one of the object member has + * a truthy value. In other words, it will check a field has + * a error message attached to it or not. + * + */ +const ifFieldsHave = (errors: FormErrors): boolean => { + return !Object.keys(errors).every((k: any) => !errors[ k ]); +}; + +const required = (value: any) => { + if (!value) { + return "This is a required field"; + } + + return undefined; +}; + +const length = (minMax: MinMax) => (value: string) => { + if (!value && minMax.min > 0) { + return "You cannot leave this blank"; + } + if (value?.length > minMax.max) { + return `Cannot exceed more than ${ minMax.max } characters.`; + } + if (value?.length < minMax.min) { + return `Should have at least ${ minMax.min } characters.`; + } + + return undefined; +}; + +const isUrl = (value: string) => { + return FormValidation.url(value) ? undefined : "This value is invalid."; +}; From cf5dacc457698247e9382fba4f8f1a7182d97c46 Mon Sep 17 00:00:00 2001 From: Shenali Date: Tue, 7 Jan 2025 21:35:22 +0530 Subject: [PATCH 05/13] Update authenticator create wizard with custom auth initiaition --- .../authenticator-create-wizard-factory.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/features/admin.connections.v1/components/create/authenticator-create-wizard-factory.tsx b/features/admin.connections.v1/components/create/authenticator-create-wizard-factory.tsx index 41a75cc7054..9e742d29b12 100644 --- a/features/admin.connections.v1/components/create/authenticator-create-wizard-factory.tsx +++ b/features/admin.connections.v1/components/create/authenticator-create-wizard-factory.tsx @@ -23,6 +23,9 @@ import React, { FC, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { CreateConnectionWizard } from "./add-connection-wizard"; +import { + CustomAuthenticationCreateWizard +} from "./custom-authentication-create-wizard"; import { EnterpriseConnectionCreateWizard } from "./enterprise-connection-create-wizard"; @@ -167,6 +170,8 @@ export const AuthenticatorCreateWizardFactory: FC ); + // TODO: use local for headings + case "custom-authentication": + return ( + { + setSelectedTemplateWithUniqueName(undefined); + setSelectedTemplate(undefined); + handleModalVisibility(false); + onWizardClose(); + } } + template={ selectedTemplateWithUniqueName } + data-componentid={ selectedTemplate?.templateId } + { ...rest } + /> + ); + case CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXPERT_MODE: return ( Date: Tue, 7 Jan 2025 21:36:18 +0530 Subject: [PATCH 06/13] Update models and constants --- .../local-authenticator-constants.ts | 25 +++ .../admin.connections.v1/models/connection.ts | 152 ++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/features/admin.connections.v1/constants/local-authenticator-constants.ts b/features/admin.connections.v1/constants/local-authenticator-constants.ts index 06d5b3724d6..650c4cdeb71 100644 --- a/features/admin.connections.v1/constants/local-authenticator-constants.ts +++ b/features/admin.connections.v1/constants/local-authenticator-constants.ts @@ -16,6 +16,8 @@ * under the License. */ +import { AuthenticationType, AuthenticationTypeDropdownOption } from "../models/connection"; + /** * This class contains the constants for the Local Authenticators. */ @@ -116,4 +118,27 @@ export class LocalAuthenticatorConstants { * Attribute key for SMS OTP Authenticator expiry time. */ public static readonly MODERATED_SMS_OTP_EXPIRY_TIME_KEY: string = "SmsOTP_ExpiryTime"; + + public static readonly AUTH_TYPES: AuthenticationTypeDropdownOption[] = [ + { + key: AuthenticationType.NONE, + text: "actions:fields.authentication.types.none.name", + value: AuthenticationType.NONE + }, + { + key: AuthenticationType.BASIC, + text: "actions:fields.authentication.types.basic.name", + value: AuthenticationType.BASIC + }, + { + key: AuthenticationType.BEARER, + text: "actions:fields.authentication.types.bearer.name", + value: AuthenticationType.BEARER + }, + { + key: AuthenticationType.API_KEY, + text: "actions:fields.authentication.types.apiKey.name", + value: AuthenticationType.API_KEY + } + ]; } diff --git a/features/admin.connections.v1/models/connection.ts b/features/admin.connections.v1/models/connection.ts index fe7ca80e5da..5d0c4eca262 100644 --- a/features/admin.connections.v1/models/connection.ts +++ b/features/admin.connections.v1/models/connection.ts @@ -264,6 +264,36 @@ export interface FederatedAuthenticatorInterface extends CommonPluggableComponen isEnabled?: boolean; isDefault?: boolean; tags?: string[]; + endpoint?: ExternalEndpoint; +} + +/** + * Captures the properties of a externally implemented local authenticator. + */ +export interface CustomAuthenticatorInterface extends StrictConnectionInterface { + name?: string; + displayName?: string; + isEnabled?: boolean; + isDefault?: boolean; + endpoint?: ExternalEndpoint; + authenticationType?: string; + description?: string; +} + +/** + * Captures the properties of a external endpoint associated with the authenticator. + */ +export interface ExternalEndpoint { + uri?: string; + authentication?: ExternalEndpointAuthentication +} + +/** + * Captures the properties of a external endpoint authentication details associated with the authenticator. + */ +export interface ExternalEndpointAuthentication { + type?: AuthenticationType; + properties?: string[] //TODO: check the object } /** @@ -449,6 +479,7 @@ export interface ConnectionTemplateItemInterface { category?: string; displayOrder?: number; idp?: ConnectionInterface; + customAuth?: CustomAuthenticatorInterface; // TODO; check this object disabled?: boolean; provisioning?: ProvisioningInterface; type?: string; @@ -767,6 +798,117 @@ export interface EnterpriseConnectionCreateWizardGeneralFormValuesInterface { name: string; } +/** + * Interface for the general form values in the custom authentication wizard. + */ +export interface CustomAuthenticationCreateWizardGeneralFormValuesInterface { + /** + * Identifier of the custom authentication. + */ + identifier: string; + /** + * Display name of the custom authentication. + */ + displayName: string; +} + +/** + * Custom authentication endpoint config form property Interface. + */ +export interface EndpointConfigFormPropertyInterface { + /** + * Endpoint Uri of the Action. + */ + endpointUri: string; + /** + * Endpoint Uri of the Action. + */ + authenticationType: string; + /** + * Username property of basic authentication. + */ + usernameAuthProperty?: string; + /** + * Password property of basic authentication. + */ + passwordAuthProperty?: string; + /** + * Access Token property of bearer authentication. + */ + accessTokenAuthProperty?: string; + /** + * Header property of apiKey authentication. + */ + headerAuthProperty?: string; + /** + * Value property of apiKey authentication. + */ + valueAuthProperty?: string; +} + +/** + * Endpoint configuration. + */ +export interface EndpointInterface { + /** + * External endpoint. + */ + uri: string; + /** + * Authentication configurations of the Action. + */ + authentication: AuthenticationInterface; +} + +/** + * Endpoint authentication configuration. + */ +interface AuthenticationInterface { + /** + * Authentication Type. + */ + type: AuthenticationType; + /** + * Authentication properties. + */ + properties: Partial; +} + +/** + * Authentication Properties. + */ +export interface AuthenticationPropertiesInterface { + /** + * Username auth property. + */ + username: string; + /** + * Password auth property. + */ + password: string; + /** + * Access Token auth property. + */ + accessToken: string; + /** + * Header auth property. + */ + header: string; + /** + * Value auth property. + */ + value: string; +} + +/** + * Interface for the authentication type dropdown options. + */ +export interface AuthenticationTypeDropdownOption { + key: AuthenticationType; + text: string; + value: AuthenticationType; +} + /** * Enum for the connection type. */ @@ -774,3 +916,13 @@ export enum ConnectionTypes { CONNECTION = "connections", IDVP = "identity-verification-providers" } + +/** + * Authentication Types. + */ +export enum AuthenticationType { + NONE = "NONE", + BASIC = "BASIC", + API_KEY = "API_KEY", + BEARER = "BEARER", +} From 6da13c1b1e7c849a3d537f3a5f10a4096092ff36 Mon Sep 17 00:00:00 2001 From: Shenali Date: Wed, 8 Jan 2025 11:04:32 +0530 Subject: [PATCH 07/13] Add emphasized content and improve final page --- .../custom-authentication-create-wizard.tsx | 493 +++--------------- 1 file changed, 58 insertions(+), 435 deletions(-) diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx index 734199247e1..622fa3f9808 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -16,12 +16,8 @@ * under the License. */ -import { SelectChangeEvent } from "@mui/material"; import Backdrop from "@mui/material/Backdrop"; -import Alert from "@oxygen-ui/react/Alert"; -import AlertTitle from "@oxygen-ui/react/AlertTitle"; import Box from "@oxygen-ui/react/Box"; -import Button from "@oxygen-ui/react/Button"; import Divider from "@oxygen-ui/react/Divider"; import Grid from "@oxygen-ui/react/Grid"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; @@ -35,12 +31,6 @@ import { addAlert } from "@wso2is/core/store"; import { URLUtils } from "@wso2is/core/utils"; import { Field, - FinalForm, - FinalFormField, - FormRenderProps, - FormSpy, - SelectFieldAdapter, - TextFieldAdapter, Wizard2, WizardPage } from "@wso2is/form"; @@ -75,7 +65,7 @@ import React, { useRef, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Icon, Grid as SemanticGrid } from "semantic-ui-react"; @@ -84,16 +74,18 @@ import { getConnectionIcons, getConnectionWizardStepIcons } from "../../configs/ import { ConnectionUIConstants } from "../../constants/connection-ui-constants"; import { LocalAuthenticatorConstants } from "../../constants/local-authenticator-constants"; import { + AuthenticationPropertiesInterface, AuthenticationType, AuthenticationTypeDropdownOption, - AuthenticationPropertiesInterface, ConnectionInterface, ConnectionTemplateInterface, CustomAuthenticationCreateWizardGeneralFormValuesInterface, - EndpointInterface, EndpointConfigFormPropertyInterface, + EndpointInterface, GenericConnectionCreateWizardPropsInterface, } from "../../models/connection"; +import "./custom-authentication-create-wizard.scss"; +import { DropDownItemInterface } from "@wso2is/form/src"; /** * Proptypes for the custom authenticator @@ -214,13 +206,6 @@ export const CustomAuthenticationCreateWizard: FC { - // if(selectedAuthenticationType) { - // setSelectedValue(selectedSubjectValue); - // setShowSubjectAttribute(selectedSubjectValue !== defaultSubjectAttribute); - // } - // }, [ selectedSubjectValue ]); - const initialValues: { NameIDType: string, RequestMethod: string, identifier: string, displayName: string } = useMemo(() => ({ NameIDType: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", @@ -324,8 +309,6 @@ export const CustomAuthenticationCreateWizard: FC { - console.log("Auth type", selectedAuthenticationType); }, [ selectedAuthenticationType ]); - const handleDropdownChange = (event, data) => { - setSelectedAuthenticationType(data.value); // Update the state with the selected value - console.log("Selected value:", data.value); // Log the selected value for debugging + const handleDropdownChange = (data) => { // TODO: [Immediate] make type safe + setSelectedAuthenticationType(data.value); }; const validateForm = (values: EndpointConfigFormPropertyInterface): Partial => { const error: Partial = {}; - console.log("value type: " + values?.authenticationType); - console.log("value uri: " + values?.endpointUri); + // TODO: use local - and update with proper local if (!values?.endpointUri) { error.endpointUri = "Empty endpoint URI"; @@ -618,15 +598,6 @@ export const CustomAuthenticationCreateWizard: FC { - // const showAuthSecretsHint = (): ReactElement => ( - // - // { - // isAuthenticationCreateState ? - // t("actions:fields.authenticationType.hint.create") - // : t("actions:fields.authenticationType.hint.update") - // } - // - // ); switch (selectedAuthenticationType) { case AuthenticationType.NONE: @@ -638,7 +609,6 @@ export const CustomAuthenticationCreateWizard: FC { - // const renderAuthenticationSection = (): ReactElement => { - // const renderAuthenticationSectionInfoBox = (): ReactElement => { - // const resolveAuthTypeDisplayName = (): string => { - // switch (authenticationType) { - // case AuthenticationType.NONE: // TODO: update with proper local - // return t("actions:fields.authentication.types.none.name"); - // case AuthenticationType.BASIC: - // return t("actions:fields.authentication.types.basic.name"); - // case AuthenticationType.BEARER: - // return t("actions:fields.authentication.types.bearer.name"); - // case AuthenticationType.API_KEY: - // return t("actions:fields.authentication.types.apiKey.name"); - // default: - // return; - // } - // }; - - // return ( - // - // - // } } - // /> - // - // - // If you are changing the authentication, be aware that the authentication secrets of - // the external endpoint need to be updated. - // - //
- // - //
- //
- // ); - // }; - - // const renderAuthenticationUpdateWidget = (): ReactElement => { - // const renderAuthentication = (): ReactElement => { - // const renderAuthenticationPropertyFields = (): ReactElement => { - // const showAuthSecretsHint = (): ReactElement => ( - // - // { - // isAuthenticationCreateState ? - // t("actions:fields.authenticationType.hint.create") - // : t("actions:fields.authenticationType.hint.update") - // } - // - // ); - - // switch (authenticationType) { - // case AuthenticationType.NONE: - // break; - // case AuthenticationType.BASIC: - // return ( - // <> - // { showAuthSecretsHint() } - // setIsShowSecret1(!isShowSecret1)) - // } } - // label={ t("actions:fields.authentication" + - // ".types.basic.properties.username.label") } - // placeholder={ t("actions:fields.authentication" + - // ".types.basic.properties.username.placeholder") } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // setIsShowSecret2(!isShowSecret2)) - // } } - // label={ t("actions:fields.authentication" + - // ".types.basic.properties.password.label") } - // placeholder={ t("actions:fields.authentication" + - // ".types.basic.properties.password.placeholder") } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // - // ); - // case AuthenticationType.BEARER: - // return ( - // <> - // { showAuthSecretsHint() } - // setIsShowSecret1(!isShowSecret1)) - // } } - // label={ t("actions:fields.authentication" + - // ".types.bearer.properties.accessToken.label") } - // placeholder={ t("actions:fields.authentication" + - // ".types.bearer.properties.accessToken.placeholder") } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // - // ); - // case AuthenticationType.API_KEY: - // return ( - // <> - // { showAuthSecretsHint() } - // - // { t("actions:fields.authentication" + - // ".types.apiKey.properties.header.hint") } - // - // ) } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // setIsShowSecret2(!isShowSecret2)) - // } } - // label={ t("actions:fields.authentication" + - // ".types.apiKey.properties.value.label") } - // placeholder={ t("actions:fields.authentication" + - // ".types.apiKey.properties.value.placeholder") } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // - // ); - // default: - // break; - // } - // }; - - // const handleAuthTypeChange = (event: SelectChangeEvent) => { - // switch (event.target.value) { - // case AuthenticationType.NONE.toString(): - // setAuthenticationType(AuthenticationType.NONE); - - // break; - // case AuthenticationType.BASIC.toString(): - // setAuthenticationType(AuthenticationType.BASIC); - - // break; - // case AuthenticationType.BEARER.toString(): - // setAuthenticationType(AuthenticationType.BEARER); - - // break; - // case AuthenticationType.API_KEY.toString(): - // setAuthenticationType(AuthenticationType.API_KEY); - - // break; - // default: - // setAuthenticationType(AuthenticationType.NONE); - // } - - // renderAuthenticationPropertyFields(); - // }; - - // return ( - // <> - // ({ - // text: t(option.text), - // value: option.value.toString() })) - // ] - // } - // onChange={ handleAuthTypeChange } - // disabled={ getFieldDisabledStatus() } - // /> - // {/* { renderAuthenticationPropertyFields() } */} - // - // ); - // }; - - // return ( - // - //
- // { renderAuthentication() } - // {/* { !isAuthenticationCreateState && ( - // - // ) } */} - //
- //
- // ); - // }; - - // // return ( !isAuthenticationUpdateState && !isAuthenticationCreateState && !(authenticationType === null) ? - // // renderAuthenticationSectionInfoBox() : renderAuthenticationUpdateWidget()); - - // return ( renderAuthenticationUpdateWidget()); - // }; - - // if (isLoading) { - // return renderLoadingPlaceholders(); - // } - - // return ( - // <> - // - // { t("actions:fields.endpoint.hint") } - // - // ) } - // component={ TextFieldAdapter } - // maxLength={ 100 } - // minLength={ 0 } - // disabled={ getFieldDisabledStatus() } - // /> - // - // - // { t("actions:fields.authentication.label") } - // - // { renderAuthenticationSection() } - // - // ); - // }; - const wizardCommonFirstPage = () => ( { @@ -1240,54 +859,60 @@ export const CustomAuthenticationCreateWizard: FC ( - - - - { "Authentication" } - - - + ({ - text: t(option.text), - value: option.value.toString() })) - ] - } - onChange={ handleDropdownChange } - enableReinitialize={ true } - data-componentid={ `${ componentId }-endpoint_authentication-dropdown` } + maxLength={ 100 } + minLength={ 0 } + data-componentid={ `${ componentId }-endpointUri` } width={ 15 } /> -
- { renderEndpointAuthPropertyFields() } -
-
+ + + { "Authentication" } + + + ({ + text: t(option.text), + value: option.value.toString() })) + ] + } + onChange={ handleDropdownChange } + enableReinitialize={ true } + data-componentid={ `${ componentId }-endpoint_authentication-dropdown` } + width={ 15 } + /> +
+ { renderEndpointAuthPropertyFields() } +
+
+
); @@ -1398,6 +1023,7 @@ export const CustomAuthenticationCreateWizard: FC => { return [ wizardCommonFirstPage(), @@ -1406,6 +1032,7 @@ export const CustomAuthenticationCreateWizard: FC { const SECOND_STEP: number = 1; @@ -1454,10 +1081,7 @@ export const CustomAuthenticationCreateWizard: FC { let docLink: string = undefined; @@ -1483,7 +1107,6 @@ export const CustomAuthenticationCreateWizard: FC Date: Fri, 10 Jan 2025 12:25:25 +0530 Subject: [PATCH 08/13] Add new icons and logos --- .../assets/images/icons/external-authentication-icon.svg | 3 +++ .../assets/images/icons/internal-user-authentication-icon.svg | 3 +++ .../images/icons/two-factor-custom-authentication-icon.svg | 3 +++ .../connections/assets/images/logos/custom-authentication.svg | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 apps/console/src/public/resources/connections/assets/images/icons/external-authentication-icon.svg create mode 100644 apps/console/src/public/resources/connections/assets/images/icons/internal-user-authentication-icon.svg create mode 100644 apps/console/src/public/resources/connections/assets/images/icons/two-factor-custom-authentication-icon.svg create mode 100644 apps/console/src/public/resources/connections/assets/images/logos/custom-authentication.svg diff --git a/apps/console/src/public/resources/connections/assets/images/icons/external-authentication-icon.svg b/apps/console/src/public/resources/connections/assets/images/icons/external-authentication-icon.svg new file mode 100644 index 00000000000..d0dff8b12de --- /dev/null +++ b/apps/console/src/public/resources/connections/assets/images/icons/external-authentication-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/console/src/public/resources/connections/assets/images/icons/internal-user-authentication-icon.svg b/apps/console/src/public/resources/connections/assets/images/icons/internal-user-authentication-icon.svg new file mode 100644 index 00000000000..1b85af24823 --- /dev/null +++ b/apps/console/src/public/resources/connections/assets/images/icons/internal-user-authentication-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/console/src/public/resources/connections/assets/images/icons/two-factor-custom-authentication-icon.svg b/apps/console/src/public/resources/connections/assets/images/icons/two-factor-custom-authentication-icon.svg new file mode 100644 index 00000000000..6470dabe961 --- /dev/null +++ b/apps/console/src/public/resources/connections/assets/images/icons/two-factor-custom-authentication-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/console/src/public/resources/connections/assets/images/logos/custom-authentication.svg b/apps/console/src/public/resources/connections/assets/images/logos/custom-authentication.svg new file mode 100644 index 00000000000..56bdab53d62 --- /dev/null +++ b/apps/console/src/public/resources/connections/assets/images/logos/custom-authentication.svg @@ -0,0 +1,3 @@ + + + From 81e91ebb30a06e1aebb73c0aa74f1587b42e84d7 Mon Sep 17 00:00:00 2001 From: Shenali Date: Fri, 10 Jan 2025 12:28:04 +0530 Subject: [PATCH 09/13] Update custom authentication create wizard css --- .../custom-authentication-create-wizard.scss | 130 +++++++ .../custom-authentication-create-wizard.tsx | 345 +++++++----------- 2 files changed, 256 insertions(+), 219 deletions(-) create mode 100644 features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss new file mode 100644 index 00000000000..37d33ffaf92 --- /dev/null +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2024, 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 + * 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. + */ + + .form-wrapper { + .form-container { + margin-top: 0px; + margin-bottom: 0px; + } + + .box-container { + background-color: #f9fafb; + border-radius: 8px; + padding: 30px; + + .box-field { + margin-top: 20px; + margin-left: 10px; + margin-right: 10px; + } + } + + .text-field-container { + margin-top: 8px; + margin-bottom: 8px; + } + + .select-field-container { + margin-bottom: 8px; + } + + .hint-text { + margin-top: 0px; + margin-bottom: 8px; + } + + .divider-container { + margin-top: 20px; + margin-bottom: 20px; + } + + .heading-container { + margin-top: 20px; + margin-bottom: 20px; + } + + .button-container { + margin-top: 30px; + } + + .alert-title { + font-weight: 100; + } + + .secondary-button { + margin-top: 10px; + } + + .placeholder { + border-radius: 8px; + margin-bottom: 8px; + + &.label { + height: 20px; + max-width: 100px; + } + + &.text-field { + height: 40px; + } + } + + .placeholder-box { + > * { + margin-bottom: 8px; + } + } +} + +.sub-template-selection { + + .sub-template-selection-container { + display: flex; + justify-content: space-between; + margin-top: 10px; + column-gap: 20px; + + .sub-template-selection-card { + flex: 1; + height: 260px; + margin: none; + white-space: normal; + + .card-text-container { + .header { + white-space: normal; + font-weight: bold; + margin: 20px; + } + .p { + display: flex; + flex-direction: column; + justify-content: space-between; + white-space: normal; + } + .main-description { + margin-bottom: 15px; + white-space: normal; + } + } + } + .sub-template-selection-card:first-child { + margin-top: 14px; // semantic-ui-react Card has set top margin of the first child to zero. We are overriding it. + } + } +} diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx index 622fa3f9808..f44a1c583d6 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -19,7 +19,6 @@ import Backdrop from "@mui/material/Backdrop"; import Box from "@oxygen-ui/react/Box"; import Divider from "@oxygen-ui/react/Divider"; -import Grid from "@oxygen-ui/react/Grid"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; import Skeleton from "@oxygen-ui/react/Skeleton"; import { FeatureAccessConfigInterface, useRequiredScopes } from "@wso2is/access-control"; @@ -70,7 +69,7 @@ import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Icon, Grid as SemanticGrid } from "semantic-ui-react"; import { createConnection, useGetConnectionTemplate } from "../../api/connections"; -import { getConnectionIcons, getConnectionWizardStepIcons } from "../../configs/ui"; +import { getConnectionWizardStepIcons } from "../../configs/ui"; import { ConnectionUIConstants } from "../../constants/connection-ui-constants"; import { LocalAuthenticatorConstants } from "../../constants/local-authenticator-constants"; import { @@ -78,14 +77,13 @@ import { AuthenticationType, AuthenticationTypeDropdownOption, ConnectionInterface, - ConnectionTemplateInterface, CustomAuthenticationCreateWizardGeneralFormValuesInterface, EndpointConfigFormPropertyInterface, EndpointInterface, GenericConnectionCreateWizardPropsInterface, } from "../../models/connection"; import "./custom-authentication-create-wizard.scss"; -import { DropDownItemInterface } from "@wso2is/form/src"; +import { ConnectionsManagementUtils } from "../../utils/connection-utils"; /** * Proptypes for the custom authenticator @@ -104,7 +102,7 @@ interface CustomAuthenticationCreateWizardProps extends * CONFIGURATION - Includes the external endpoint configuration details. */ enum WizardSteps { - AUTHENTICATION_TYPE = "Authentication Type", + AUTHENTICATION_TYPE = "Authentication Type", // TODO: update the authentication type image GENERAL_SETTINGS = "General Settings", CONFIGURATION = "Configuration" } @@ -146,8 +144,7 @@ export const CustomAuthenticationCreateWizard: FC(true); const [ isAuthenticationUpdateState, setIsAuthenticationUpdateState ] = useState(false); - const [ isLoading, setIsLoading ] = useState(false); + // const [ isLoading, setIsLoading ] = useState(false); const [ authenticationType, setAuthenticationType ] = useState(null); const [ selectedAuthenticationType, setSelectedAuthenticationType ] = useState(); // const [ selectedEndpointAuthType, setSelectedEndpointAuthType ] = useState(); @@ -313,7 +310,7 @@ export const CustomAuthenticationCreateWizard: FC { }, [ selectedAuthenticationType ]); - const handleDropdownChange = (data) => { // TODO: [Immediate] make type safe + const handleDropdownChange = (event, data) => { // TODO: [Immediate] make type safe + console.log("Data: " + data); setSelectedAuthenticationType(data.value); }; @@ -599,6 +597,8 @@ export const CustomAuthenticationCreateWizard: FC { + console.log("selectedAuthenticationType: " + selectedAuthenticationType); + switch (selectedAuthenticationType) { case AuthenticationType.NONE: break; @@ -729,82 +729,96 @@ export const CustomAuthenticationCreateWizard: FC
- - - - External (Federated) User Authentication } - selected={ selectedAuthenticator === "external" } - onClick={ () => setSelectedAuthenticator("external") } - imageSize="x30" - imageOptions={ { - relaxed: true, - square: false, - width: "auto" - } } - contentTopBorder={ false } - showTooltips={ true } - data-componentid={ `${ componentId }-form-wizard-external-custom-authentication- - selection-card` } - /> - Internal User Authentication } - selected={ selectedAuthenticator === "internal" } - onClick={ () => setSelectedAuthenticator("internal") } - imageSize="x30" - imageOptions={ { - relaxed: true, - square: false, - width: "auto" - } } - showTooltips={ true } - disabled={ false } - overlay={ renderDimmerOverlay() } - contentTopBorder={ false } - renderDisabledItemsAsGrayscale={ false } - overlayOpacity={ 0.6 } - data-componentid={ `${ componentId }-form-wizard-internal-custom-authentication- - selection-card` } - /> - 2FA Authentication } - selected={ selectedAuthenticator === "two-factor" } - onClick={ () => setSelectedAuthenticator("two-factor") } - imageSize="x30" - imageOptions={ { - relaxed: true, - square: false, - width: "auto" - } } - showTooltips={ true } - disabled={ false } - overlay={ renderDimmerOverlay() } - contentTopBorder={ false } - renderDisabledItemsAsGrayscale={ false } - overlayOpacity={ 0.6 } - data-componentid={ `${ componentId }-form-wizard-two-factor-custom-authentication- - selection-card` } - /> - - + +
+ + External (Federated) User Authentication +
) } + description={ + (
+

Authenticate and provision federated users.

+

Eg: Social Login, Enterprise IdP

+
) + } + contentTopBorder={ false } + selected={ selectedAuthenticator === "external" } + onClick={ () => setSelectedAuthenticator("external") } + imageSize="x60" + imageOptions={ { + relaxed: "very", + square: false, + width: "auto" + } } + showTooltips={ true } + overlay={ renderDimmerOverlay() } + overlayOpacity={ 0.6 } + data-componentid={ `${ componentId }-form-wizard-external-custom-authentication- + selection-card` } + /> + + Internal User Authentication +
) } + description={ + (
+

+ Collect identifier and authenticate user accounts managed in the organization. +

+

Eg: Username & Password, Email OTP

+
) + } + selected={ selectedAuthenticator === "internal" } + onClick={ () => setSelectedAuthenticator("internal") } + imageSize="x60" + showTooltips={ true } + contentTopBorder={ false } + overlay={ renderDimmerOverlay() } + overlayOpacity={ 0.6 } + data-componentid={ `${ componentId }-form-wizard-internal-custom-authentication- + selection-card` } + /> + + 2FA Authentication + ) } + description={ + (
+

+ Only verify users in a second or later step in the login flow. +

+

Eg: TOTP

+
) + } + selected={ selectedAuthenticator === "two-factor" } + onClick={ () => setSelectedAuthenticator("two-factor") } + imageSize="x60" + showTooltips={ true } + overlay={ renderDimmerOverlay() } + overlayOpacity={ 0.6 } + contentTopBorder={ false } + data-componentid={ `${ componentId }-form-wizard-two-factor-custom-authentication- + selection-card` } + /> +
); @@ -859,13 +873,15 @@ export const CustomAuthenticationCreateWizard: FC ( @@ -916,112 +932,6 @@ export const CustomAuthenticationCreateWizard: FC ); - // ### Here the configurationsPage is written with FinalForm - // const configurationsPage = () => ( - // { - // handleSubmit(values, form.getState().dirtyFields); } - // } - // validate={ validateForm } - // // initialValues={ endpointInitialValues } - // render={ ({ handleSubmit, form }: FormRenderProps) => ( - //
- // - //
- // { renderFormFields() } - // { !isLoading && ( - // - // ) } - //
- //
- // - // { ({ values }: { values: EndpointConfigFormPropertyInterface }) => { - // if (!isAuthenticationUpdateState) { - // form.change("authenticationType", - // endpointInitialValues?.authenticationType); - // switch (authenticationType) { - // case AuthenticationType.BASIC: - // delete values.usernameAuthProperty; - // delete values.passwordAuthProperty; - - // break; - // case AuthenticationType.BEARER: - // delete values.accessTokenAuthProperty; - - // break; - // case AuthenticationType.API_KEY: - // delete values.headerAuthProperty; - // delete values.valueAuthProperty; - - // break; - // default: - // break; - // } - // } - - // // Clear inputs of property field values of other authentication types. - // switch (authenticationType) { - // case AuthenticationType.BASIC: - // delete values.accessTokenAuthProperty; - // delete values.headerAuthProperty; - // delete values.valueAuthProperty; - - // break; - // case AuthenticationType.BEARER: - // delete values.usernameAuthProperty; - // delete values.passwordAuthProperty; - // delete values.headerAuthProperty; - // delete values.valueAuthProperty; - - // break; - // case AuthenticationType.API_KEY: - // delete values.usernameAuthProperty; - // delete values.passwordAuthProperty; - // delete values.accessTokenAuthProperty; - - // break; - // case AuthenticationType.NONE: - // delete values.usernameAuthProperty; - // delete values.passwordAuthProperty; - // delete values.headerAuthProperty; - // delete values.valueAuthProperty; - // delete values.accessTokenAuthProperty; - - // break; - // default: - - // break; - // } - - // return null; - // } } - // - //
- // ) } - // > - //
- // ); - // Resolvers const resolveWizardPages = (): Array => { @@ -1032,6 +942,23 @@ export const CustomAuthenticationCreateWizard: FC { + // TODO: check if there is a better way to access the field input label "Identifier" + return ( +
+ Identifier +

+ We recommend using a URI as the identifier, but you do not need to make the URI + publicly available since WSO2 Identity Server will not access your API. + WSO2 Identity Server will use this identifier value as the audience(aud) + claim in the issued JWT tokens. + This field should be unique; once created, it is not editable. +

+
+ ); + }; + + // TODO: need to update const resolveHelpPanel = () => { @@ -1039,29 +966,6 @@ export const CustomAuthenticationCreateWizard: FC { - return id === (selectedAuthenticator === "external" - ? "external-custom-authentication" - : selectedAuthenticator === "internal" - ? "internal-custom-authentication" - : "two-factor-custom-authentication" - ); - } - )); - - // TODO: update this with correct wizard helps - if (!subTemplate?.content?.wizardHelp) return null; - - // let { wizardHelp: WizardHelp } = subTemplate?.content; - - // if (selectedAuthenticator === "external" && selectedSamlConfigMode === "file") { - // WizardHelp = subTemplate.content.fileBasedHelpPanel; - // } - return ( }> - {/* */} + @@ -1124,7 +1028,10 @@ export const CustomAuthenticationCreateWizard: FC
- { resolveHelpPanel() } + { (resolveHelpPanel()) } ); From 93c655ff320cb686b3efae9176794e3494ff1f60 Mon Sep 17 00:00:00 2001 From: Shenali Date: Fri, 10 Jan 2025 12:30:15 +0530 Subject: [PATCH 10/13] Improve custom authentication group json --- .../meta/templates-meta/groups/custom-authentication.json | 2 +- features/admin.connections.v1/models/connection.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json index 71036a5bc66..0955d9675fd 100644 --- a/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json +++ b/features/admin.connections.v1/meta/templates-meta/groups/custom-authentication.json @@ -5,7 +5,7 @@ "displayOrder": 12, "id": "custom-authentication", "docLink": "", - "image": "assets/images/logos/expert.svg", + "image": "assets/images/logos/custom-authentication.svg", "name": "Custom Authentication", "services": [], "disabled": false, diff --git a/features/admin.connections.v1/models/connection.ts b/features/admin.connections.v1/models/connection.ts index 5d0c4eca262..2b6c7318132 100644 --- a/features/admin.connections.v1/models/connection.ts +++ b/features/admin.connections.v1/models/connection.ts @@ -289,7 +289,7 @@ export interface ExternalEndpoint { } /** - * Captures the properties of a external endpoint authentication details associated with the authenticator. + * Captures the authentication properties of an external endpoint associated with the authenticator. */ export interface ExternalEndpointAuthentication { type?: AuthenticationType; From 73ae40c40766dc46cccf892837735d14eaa555a9 Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 16 Jan 2025 09:38:18 +0530 Subject: [PATCH 11/13] Add i18n related resources --- modules/i18n/src/constants.ts | 5 +++ .../namespaces/custom-auth-connection-ns.ts | 42 ++++++++++++++++++ modules/i18n/src/models/namespaces/index.ts | 1 + modules/i18n/src/translations/en-US/meta.ts | 3 +- .../en-US/portals/custom-auth-connection.ts | 44 +++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 modules/i18n/src/models/namespaces/custom-auth-connection-ns.ts create mode 100644 modules/i18n/src/translations/en-US/portals/custom-auth-connection.ts diff --git a/modules/i18n/src/constants.ts b/modules/i18n/src/constants.ts index 62cd53c1d71..70c49fede10 100644 --- a/modules/i18n/src/constants.ts +++ b/modules/i18n/src/constants.ts @@ -338,4 +338,9 @@ export class I18nModuleConstants { * Remote User Stores namespace. */ public static readonly REMOTE_USER_STORES_NAMESPACE: string = "remoteUserStores"; + + /** + * Custom Authentication namespace. + */ + public static readonly CUSTOM_AUTHENTICATION_NAMESPACE: string = "customAuthentication"; } diff --git a/modules/i18n/src/models/namespaces/custom-auth-connection-ns.ts b/modules/i18n/src/models/namespaces/custom-auth-connection-ns.ts new file mode 100644 index 00000000000..58e6f565588 --- /dev/null +++ b/modules/i18n/src/models/namespaces/custom-auth-connection-ns.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024, 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 + * 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. + */ + +export interface customAuthConnectionNS { + fields: { + createWizard: { + authenticationTypeStep: { + label: string; + cardExternalAuthentication: { + header: string; + mainDescription: string; + examples: string; + }; + cardInternalUserAuthentication: { + header: string; + mainDescription: string; + examples: string; + }; + twoFactorAuthentication: { + header: string; + mainDescription: string; + examples: string; + } + }; + }; + }; +}; diff --git a/modules/i18n/src/models/namespaces/index.ts b/modules/i18n/src/models/namespaces/index.ts index 1db9f9caeec..cb3444e1931 100644 --- a/modules/i18n/src/models/namespaces/index.ts +++ b/modules/i18n/src/models/namespaces/index.ts @@ -64,3 +64,4 @@ export * from "./actions-ns"; export * from "./tenants-ns"; export * from "./sms-templates-ns"; export * from "./remote-user-stores-ns"; +export * from "./custom-auth-connection-ns"; diff --git a/modules/i18n/src/translations/en-US/meta.ts b/modules/i18n/src/translations/en-US/meta.ts index 911b94160e6..a1e6085d237 100644 --- a/modules/i18n/src/translations/en-US/meta.ts +++ b/modules/i18n/src/translations/en-US/meta.ts @@ -70,6 +70,7 @@ export const meta: LocaleMeta = { I18nModuleConstants.ACTIONS_NAMESPACE, I18nModuleConstants.TENANTS_NAMESPACE, I18nModuleConstants.SMS_TEMPLATES_NAMESPACE, - I18nModuleConstants.REMOTE_USER_STORES_NAMESPACE + I18nModuleConstants.REMOTE_USER_STORES_NAMESPACE, + I18nModuleConstants.CUSTOM_AUTHENTICATION_NAMESPACE ] }; diff --git a/modules/i18n/src/translations/en-US/portals/custom-auth-connection.ts b/modules/i18n/src/translations/en-US/portals/custom-auth-connection.ts new file mode 100644 index 00000000000..52709a2026a --- /dev/null +++ b/modules/i18n/src/translations/en-US/portals/custom-auth-connection.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024, 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 + * 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 { customAuthConnectionNS } from "../../../models"; + +export const customAuthentication: customAuthConnectionNS = { + fields: { + createWizard: { + authenticationTypeStep: { + cardExternalAuthentication: { + examples: "Eg: Social Login, Enterprise IdP", + header: "External (Federated) User Authentication", + mainDescription: "Authenticate and provision federated users." + }, + cardInternalUserAuthentication: { + examples: "Eg: Username & Password, Email OTP", + header: "Internal User Authentication", + mainDescription: "Collect identifier and authenticate user accounts managed in the organization." + }, + label: "Select the authentication type you are implementing", + twoFactorAuthentication: { + examples: "Eg: TOTP", + header: "2FA Authentication", + mainDescription: "Only verify users in a second or later step in the login flow." + } + } + } + } +}; From 0741050fc9b3ace3c6d9806ff258c3eef25e3797 Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 16 Jan 2025 10:32:15 +0530 Subject: [PATCH 12/13] Format the custom authenticator create wizard --- .../custom-authentication-create-wizard.scss | 8 +- .../custom-authentication-create-wizard.tsx | 470 +++++------------- 2 files changed, 120 insertions(+), 358 deletions(-) diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss index 37d33ffaf92..a84edf931a6 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.scss @@ -16,8 +16,8 @@ * under the License. */ - .form-wrapper { - .form-container { + .wizard-wrapper { + .wizard-container { margin-top: 0px; margin-bottom: 0px; } @@ -122,6 +122,10 @@ white-space: normal; } } + + .card-image-container { + height: 10px; + } } .sub-template-selection-card:first-child { margin-top: 14px; // semantic-ui-react Card has set top margin of the first child to zero. We are overriding it. diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx index f44a1c583d6..d8d2997ce56 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -20,8 +20,6 @@ import Backdrop from "@mui/material/Backdrop"; import Box from "@oxygen-ui/react/Box"; import Divider from "@oxygen-ui/react/Divider"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; -import Skeleton from "@oxygen-ui/react/Skeleton"; -import { FeatureAccessConfigInterface, useRequiredScopes } from "@wso2is/access-control"; import { AppState, EventPublisher } from "@wso2is/admin.core.v1"; import { ModalWithSidePanel } from "@wso2is/admin.core.v1/components"; import { IdentityAppsError } from "@wso2is/core/errors"; @@ -102,7 +100,7 @@ interface CustomAuthenticationCreateWizardProps extends * CONFIGURATION - Includes the external endpoint configuration details. */ enum WizardSteps { - AUTHENTICATION_TYPE = "Authentication Type", // TODO: update the authentication type image + AUTHENTICATION_TYPE = "Authentication Type", // TODO: update the authentication type step icon GENERAL_SETTINGS = "General Settings", CONFIGURATION = "Configuration" } @@ -114,24 +112,6 @@ interface WizardStepInterface { name: WizardSteps; } -/** - * Prop types for the endpoint configuration form component. - */ -interface EndpointConfigFormInterface extends IdentifiableComponentInterface { - /** - * Endpoint's initial values. - */ - initialValues: EndpointConfigFormInterface; - /** - * Flag for loading state. - */ - isLoading?: boolean; - /** - * Specifies action creation state. - */ - isCreateFormState: boolean; -} - type AvailableCustomAuthentications = "external" | "internal" | "two-factor"; type MinMax = { min: number; max: number }; type FormErrors = { [ key: string ]: string }; @@ -144,7 +124,7 @@ export const CustomAuthenticationCreateWizard: FC state.config.ui.features.actions); - const hasActionUpdatePermissions: boolean = useRequiredScopes(endpointFeatureConfig?.scopes?.update); - const hasActionCreatePermissions: boolean = useRequiredScopes(endpointFeatureConfig?.scopes?.create); - const eventPublisher: EventPublisher = EventPublisher.getInstance(); const { @@ -211,57 +186,6 @@ export const CustomAuthenticationCreateWizard: FC { - // return { - // authenticationType: action?.endpoint?.authentication?.type.toString(), - // endpointUri: action?.endpoint?.uri, - // id: action?.id, - // name: action?.name - // }; - // }, [action]); // TODO: add dep - - const endpointInitialValues: EndpointConfigFormPropertyInterface = useMemo(() => { - return { - authenticationType: "", - endpointUri: "", - id: "" - }; - }, []); - - /** - * The following useEffect is used to set the current Action Authentication Type. - */ - useEffect(() => { - if (!initialValues?.identifier) { - setIsAuthenticationCreateState(true); - } else { - // setAuthenticationType(endpointInitialValues.authenticationType as AuthenticationType); - setIsAuthenticationUpdateState(false); - } - }, [ initialValues ]); - - const renderInputAdornmentOfSecret = (showSecret: boolean, onClick: () => void): ReactElement => ( - - - - ); - const getWizardSteps: () => WizardStepInterface[] = () => { return [ { @@ -282,6 +206,20 @@ export const CustomAuthenticationCreateWizard: FC void): ReactElement => ( + + + + ); + const renderDimmerOverlay = (): ReactNode => { return ( @@ -290,7 +228,8 @@ export const CustomAuthenticationCreateWizard: FC) => { eventPublisher.publish("connections-finish-adding-connection", { @@ -403,209 +339,22 @@ export const CustomAuthenticationCreateWizard: FC { - }, [ selectedAuthenticationType ]); - - const handleDropdownChange = (event, data) => { // TODO: [Immediate] make type safe - console.log("Data: " + data); - setSelectedAuthenticationType(data.value); - }; - - const validateForm = (values: EndpointConfigFormPropertyInterface): - Partial => { const error: Partial = {}; - - - // TODO: use local - and update with proper local - if (!values?.endpointUri) { - error.endpointUri = "Empty endpoint URI"; - } - if (URLUtils.isURLValid(values?.endpointUri)) { - if (!(URLUtils.isHttpsUrl(values?.endpointUri))) { - error.endpointUri = t("actions:fields.endpoint.validations.notHttps"); - } - } else { - error.endpointUri = t("actions:fields.endpoint.validations.invalidUrl"); - } - - if (!selectedAuthenticationType) { - error.authenticationType = t("actions:fields.authenticationType.validations.empty"); - } - - const apiKeyHeaderRegex: RegExp = /^[a-zA-Z0-9][a-zA-Z0-9-.]+$/; - - switch (authenticationType) { - case AuthenticationType.BASIC: - if (isAuthenticationCreateState || isAuthenticationUpdateState || - values?.usernameAuthProperty || values?.passwordAuthProperty) { - if (!values?.usernameAuthProperty) { - error.usernameAuthProperty = t("actions:fields.authentication." + - "types.basic.properties.username.validations.empty"); - } - if (!values?.passwordAuthProperty) { - error.passwordAuthProperty = t("actions:fields.authentication." + - "types.basic.properties.password.validations.empty"); - } - } - - break; - case AuthenticationType.BEARER: - if (isAuthenticationCreateState || isAuthenticationUpdateState) { - if (!values?.accessTokenAuthProperty) { - error.accessTokenAuthProperty = t("actions:fields.authentication." + - "types.bearer.properties.accessToken.validations.empty"); - } - } - - break; - case AuthenticationType.API_KEY: - if (isAuthenticationCreateState || isAuthenticationUpdateState|| - values?.headerAuthProperty || values?.valueAuthProperty) { - if (!values?.headerAuthProperty) { - error.headerAuthProperty = t("actions:fields.authentication." + - "types.apiKey.properties.header.validations.empty"); - } - if (!apiKeyHeaderRegex.test(values?.headerAuthProperty)) { - error.headerAuthProperty = t("actions:fields.authentication." + - "types.apiKey.properties.header.validations.invalid"); - } - if (!values?.valueAuthProperty) { - error.valueAuthProperty = t("actions:fields.authentication." + - "types.apiKey.properties.value.validations.empty"); - } - } - - break; - default: - break; - } - - return error; - }; - - const handleSubmit = ( - values: EndpointConfigFormPropertyInterface, - changedFields: EndpointConfigFormPropertyInterface) => - { - const authProperties: Partial = {}; - - if (isAuthenticationCreateState || isAuthenticationUpdateState) { - switch (authenticationType) { - case AuthenticationType.BASIC: - authProperties.username = values.usernameAuthProperty; - authProperties.password = values.passwordAuthProperty; - - break; - case AuthenticationType.BEARER: - authProperties.accessToken = values.accessTokenAuthProperty; - - break; - case AuthenticationType.API_KEY: - authProperties.header = values.headerAuthProperty; - authProperties.value = values.valueAuthProperty; - - break; - case AuthenticationType.NONE: - break; - default: - break; - } - } - - if (isAuthenticationCreateState) { - const endpoint: EndpointInterface ={ - authentication: { - properties: authProperties, - type: authenticationType - }, - uri: values.endpointUri - }; - - setIsSubmitting(true); - // TODO: [ Immediate ] add creation API - - // createAction(actionTypeApiPath, actionValues) - // .then(() => { - // handleSuccess(ActionsConstants.CREATE); - // mutateActions(); - // }) - // .catch((error: AxiosError) => { - // handleError(error, ActionsConstants.CREATE); - // }) - // .finally(() => { - // setIsSubmitting(false); - // }); - } else { - // Update endpoint details - const endpoint: EndpointInterface = { - authentication: isAuthenticationUpdateState ? { - properties: authProperties, - type: authenticationType - } : undefined, - uri: changedFields?.endpointUri ? values.endpointUri : undefined - }; - - setIsSubmitting(true); - // TODO: [ Immediate ] add creation API - - // updateAction(actionTypeApiPath, initialValues.id, updatingValues) - // .then(() => { - // handleSuccess(ActionsConstants.UPDATE); - // setIsAuthenticationUpdateFormState(false); - // mutateAction(); - // }) - // .catch((error: AxiosError) => { - // handleError(error, ActionsConstants.UPDATE); - // }) - // .finally(() => { - // setIsSubmitting(false); - // }); - } - }; - - /** - * This is called when the Change Authentication button is pressed. - */ - const handleAuthenticationChange = (): void => { - setIsAuthenticationUpdateState(true); - }; - /** - * This is called when the cancel button is pressed. + * This method handles endpoint authentication type dropdown changes. + * @param event event associated with the dropdown change. + * @param data data changed by the event */ - const handleAuthenticationChangeCancel = (): void => { - setAuthenticationType(endpointInitialValues?.authenticationType as AuthenticationType); - setIsAuthenticationUpdateState(false); - }; - - - const getFieldDisabledStatus = (): boolean => { - if (isAuthenticationCreateState) { - return !hasActionCreatePermissions; - } else { - return !hasActionUpdatePermissions; - } + const handleDropdownChange = (event, data) => { // TODO: Make type safe + setSelectedAuthenticationType(data.value); }; - const renderLoadingPlaceholders = (): ReactElement => ( - - - - - - - ); - const renderEndpointAuthPropertyFields = (): ReactElement => { - - console.log("selectedAuthenticationType: " + selectedAuthenticationType); - switch (selectedAuthenticationType) { case AuthenticationType.NONE: break; - case AuthenticationType.BASIC: // TODO: [Immediate] check inputType + case AuthenticationType.BASIC: return ( <> - {/* { showAuthSecretsHint() } */} - {/* { showAuthSecretsHint() } */} - {/* { showAuthSecretsHint() } */} => { const error: Partial = {}; + + if (!values?.endpointUri) { + error.endpointUri = "Empty endpoint URI"; + } + if (URLUtils.isURLValid(values?.endpointUri)) { + if (!(URLUtils.isHttpsUrl(values?.endpointUri))) { + error.endpointUri = "The entered URL is not HTTPS. Please add a valid URL."; + } + } else { + error.endpointUri = "Please enter a valid URL."; + } + + if (!selectedAuthenticationType) { + error.authenticationType = "Endpoint is a required field."; + } + + const apiKeyHeaderRegex: RegExp = /^[a-zA-Z0-9][a-zA-Z0-9-.]+$/; + + switch (authenticationType) { + case AuthenticationType.BASIC: + if (isAuthenticationCreateState || isAuthenticationUpdateState || + values?.usernameAuthProperty || values?.passwordAuthProperty) { + if (!values?.usernameAuthProperty) { + error.usernameAuthProperty = "Username is a required field."; + } + if (!values?.passwordAuthProperty) { + error.passwordAuthProperty ="Password is a required field."; + } + } + + break; + case AuthenticationType.BEARER: + if (isAuthenticationCreateState || isAuthenticationUpdateState) { + if (!values?.accessTokenAuthProperty) { + error.accessTokenAuthProperty = "Access Token is a required field."; + } + } + + break; + case AuthenticationType.API_KEY: + if (isAuthenticationCreateState || isAuthenticationUpdateState|| + values?.headerAuthProperty || values?.valueAuthProperty) { + if (!values?.headerAuthProperty) { + error.headerAuthProperty = "Header is a required field."; + } + if (!apiKeyHeaderRegex.test(values?.headerAuthProperty)) { + error.headerAuthProperty = "Please choose a valid header name that" + + "adheres to the given guidelines."; + } + if (!values?.valueAuthProperty) { + error.valueAuthProperty = "Value is a required field."; + } + } + + break; + default: + break; + } + + return error; + }; + + // Wizard Step 1 const wizardCommonFirstPage = () => ( { @@ -823,17 +635,17 @@ export const CustomAuthenticationCreateWizard: FC ); - // TODO: check max and min len of two fields + // Wizard Step 2 const generalSettingsPage = () => ( { const errors: FormErrors = {}; if (!FormValidation.identifier(values.identifier)) { - errors.identifier = "Invalid Identifier"; // TODO: local message + errors.identifier = "Invalid Identifier"; } if (!FormValidation.isValidResourceName(values.displayName)) { - errors.displayName = "Invalid Display Name"; // TODO: local message + errors.displayName = "Invalid Display Name"; } setNextShouldBeDisabled(ifFieldsHave(errors)); @@ -872,14 +684,14 @@ export const CustomAuthenticationCreateWizard: FC ); - // Final Page - // TODO: remove border of the emphasized segment? + // Wizard Step 3 + // Should we remove the border of the emphasized segment? const configurationsPage = () => ( ); + // Resolvers const resolveWizardPages = (): Array => { @@ -942,8 +755,15 @@ export const CustomAuthenticationCreateWizard: FC { - // TODO: check if there is a better way to access the field input label "Identifier" return (
Identifier @@ -958,9 +778,7 @@ export const CustomAuthenticationCreateWizard: FC { + const resolveWizardHelpPanel = () => { const SECOND_STEP: number = 1; @@ -971,9 +789,6 @@ export const CustomAuthenticationCreateWizard: FC -
- Help -
}> @@ -985,44 +800,19 @@ export const CustomAuthenticationCreateWizard: FC { - let docLink: string = undefined; - - if (selectedAuthenticator === "external") { - docLink = getLink("develop.connections.newConnection.enterprise.samlLearnMore"); - } - - if (selectedAuthenticator === "internal") { - docLink = getLink("develop.connections.newConnection.enterprise.oidcLearnMore"); - } - - if (selectedAuthenticator === "two-factor") { - docLink = getLink("develop.connections.newConnection.enterprise.oidcLearnMore"); - } - - return ( - - { t("common:learnMore") } - - ); - }; - - // Start: Modal + // Final modal + // TODO: Update documentation links. return ( - { /*Modal header*/ } @@ -1041,13 +831,12 @@ export const CustomAuthenticationCreateWizard: FC { subTitle } - { resolveDocumentationLink() } + {/* { resolveDocumentationLink() } */} ) }
- { /*Modal body content*/ } - { /*Modal actions*/ } @@ -1144,15 +932,14 @@ export const CustomAuthenticationCreateWizard: FC - { (resolveHelpPanel()) } + { (resolveWizardHelpPanel()) } ); }; /** - * Default props for the custom authenticator - * creation wizard. + * Default props for the custom authenticator create wizard. */ CustomAuthenticationCreateWizard.defaultProps = { currentStep: 0, @@ -1163,9 +950,6 @@ CustomAuthenticationCreateWizard.defaultProps = { // General constants const EMPTY_STRING: string = ""; -// Validation Functions. -// FIXME: These will be removed in the future when -// form module validation gets to a stable state. /** * Given a {@link FormErrors} object, it will check whether @@ -1178,29 +962,3 @@ const EMPTY_STRING: string = ""; const ifFieldsHave = (errors: FormErrors): boolean => { return !Object.keys(errors).every((k: any) => !errors[ k ]); }; - -const required = (value: any) => { - if (!value) { - return "This is a required field"; - } - - return undefined; -}; - -const length = (minMax: MinMax) => (value: string) => { - if (!value && minMax.min > 0) { - return "You cannot leave this blank"; - } - if (value?.length > minMax.max) { - return `Cannot exceed more than ${ minMax.max } characters.`; - } - if (value?.length < minMax.min) { - return `Should have at least ${ minMax.min } characters.`; - } - - return undefined; -}; - -const isUrl = (value: string) => { - return FormValidation.url(value) ? undefined : "This value is invalid."; -}; From 9a1c5995294a53376cc8461f7a68ed2483aadd6c Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 16 Jan 2025 10:49:24 +0530 Subject: [PATCH 13/13] Improve endpoint authentication type state handling --- .../custom-authentication-create-wizard.tsx | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx index d8d2997ce56..351cf55964b 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -20,7 +20,7 @@ import Backdrop from "@mui/material/Backdrop"; import Box from "@oxygen-ui/react/Box"; import Divider from "@oxygen-ui/react/Divider"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; -import { AppState, EventPublisher } from "@wso2is/admin.core.v1"; +import { EventPublisher } from "@wso2is/admin.core.v1"; import { ModalWithSidePanel } from "@wso2is/admin.core.v1/components"; import { IdentityAppsError } from "@wso2is/core/errors"; import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; @@ -33,7 +33,6 @@ import { } from "@wso2is/form"; import { ContentLoader, - DocumentationLink, EmphasizedSegment, GenericIcon, Heading, @@ -42,7 +41,6 @@ import { PrimaryButton, SelectionCard, Steps, - useDocumentation, useWizardAlert } from "@wso2is/react-components"; import { FormValidation } from "@wso2is/validation"; @@ -63,22 +61,20 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { Dispatch } from "redux"; -import { Icon, Grid as SemanticGrid } from "semantic-ui-react"; +import { DropdownProps, Icon, Grid as SemanticGrid } from "semantic-ui-react"; import { createConnection, useGetConnectionTemplate } from "../../api/connections"; import { getConnectionWizardStepIcons } from "../../configs/ui"; import { ConnectionUIConstants } from "../../constants/connection-ui-constants"; import { LocalAuthenticatorConstants } from "../../constants/local-authenticator-constants"; import { - AuthenticationPropertiesInterface, AuthenticationType, AuthenticationTypeDropdownOption, ConnectionInterface, CustomAuthenticationCreateWizardGeneralFormValuesInterface, EndpointConfigFormPropertyInterface, - EndpointInterface, - GenericConnectionCreateWizardPropsInterface, + GenericConnectionCreateWizardPropsInterface } from "../../models/connection"; import "./custom-authentication-create-wizard.scss"; import { ConnectionsManagementUtils } from "../../utils/connection-utils"; @@ -113,7 +109,6 @@ interface WizardStepInterface { } type AvailableCustomAuthentications = "external" | "internal" | "two-factor"; -type MinMax = { min: number; max: number }; type FormErrors = { [ key: string ]: string }; export const CustomAuthenticationCreateWizard: FC = ( @@ -139,19 +134,14 @@ export const CustomAuthenticationCreateWizard: FC(false); const [ isShowSecret1, setIsShowSecret1 ] = useState(false); const [ isShowSecret2, setIsShowSecret2 ] = useState(false); - const [ isAuthenticationCreateState, setIsAuthenticationCreateState ] = useState(true); - const [ isAuthenticationUpdateState, setIsAuthenticationUpdateState ] = useState(false); // const [ isLoading, setIsLoading ] = useState(false); const [ authenticationType, setAuthenticationType ] = useState(null); - const [ selectedAuthenticationType, setSelectedAuthenticationType ] = useState(); - // const [ selectedEndpointAuthType, setSelectedEndpointAuthType ] = useState(); // Dynamic UI state const [ nextShouldBeDisabled, setNextShouldBeDisabled ] = useState(true); const dispatch: Dispatch = useDispatch(); const { t } = useTranslation(); - const { getLink } = useDocumentation(); const eventPublisher: EventPublisher = EventPublisher.getInstance(); @@ -178,7 +168,7 @@ export const CustomAuthenticationCreateWizard: FC ({ NameIDType: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", RequestMethod: "post", @@ -341,15 +331,15 @@ export const CustomAuthenticationCreateWizard: FC { // TODO: Make type safe - setSelectedAuthenticationType(data.value); + const handleDropdownChange = (event: React.MouseEvent, data: DropdownProps) => { + setAuthenticationType(data.value as AuthenticationType); }; const renderEndpointAuthPropertyFields = (): ReactElement => { - switch (selectedAuthenticationType) { + switch (authenticationType) { case AuthenticationType.NONE: break; case AuthenticationType.BASIC: @@ -481,7 +471,7 @@ export const CustomAuthenticationCreateWizard: FC ({