diff --git a/.changeset/quick-pears-impress.md b/.changeset/quick-pears-impress.md new file mode 100644 index 00000000000..f3554324249 --- /dev/null +++ b/.changeset/quick-pears-impress.md @@ -0,0 +1,6 @@ +--- +"@wso2is/features": minor +"@wso2is/console": patch +--- + +Add SMS OTP configurations for the password recovery connector edit page. diff --git a/apps/console/src/extensions/i18n/models/extensions.ts b/apps/console/src/extensions/i18n/models/extensions.ts index f07a6c158bd..506c7005921 100755 --- a/apps/console/src/extensions/i18n/models/extensions.ts +++ b/apps/console/src/extensions/i18n/models/extensions.ts @@ -3035,10 +3035,24 @@ export interface Extensions { form: { fields: { enable: FormAttributes; + enableSMSBasedRecovery: FormAttributes; + enableEmailBasedRecovery: FormAttributes; expiryTime: FormAttributes; notifySuccess: FormAttributes; - }; - }; + maxResendCount: FormAttributes; + maxFailedAttemptCount: FormAttributes; + smsOtpExpiryTime: FormAttributes; + passwordRecoveryOtpUseUppercase: FormAttributes; + passwordRecoveryOtpUseLowercase: FormAttributes; + passwordRecoveryOtpUseNumeric: FormAttributes; + passwordRecoveryOtpLength: FormAttributes; + }; + }; + recoveryOptionSubHeadingEmailLink: string; + recoveryOptionSubHeadingSMS: string; + recoveryOptionHeading: string; + otpConfigHeading: string; + failedAttemptConfigHeading: string; connectorDescription: string; heading: string; notification: { diff --git a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts index 50fa6ef5d65..62a7d018e72 100755 --- a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts +++ b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts @@ -3554,6 +3554,10 @@ export const extensions: Extensions = { hint: "Enabling this will let the users reset their password using an email.", label: "Enable" }, + enableSMSBasedRecovery: { + hint: "This specifies whether to send an SMS OTP to the mobile.", + label: "Enable SMS based recovery" + }, expiryTime: { hint: "Password recovery link expiry time in minutes.", label: "Recovery link expiry time", @@ -3573,9 +3577,80 @@ export const extensions: Extensions = { "This specifies whether to notify the user via an email when password " + "recovery is successful.", label: "Notify on successful recovery" + }, + maxResendCount: { + hint: "Password recovery maximum resend count.", + label: "Maximum resend attempts count", + placeholder: "Enter max resend count", + validations: { + invalid: "Password recovery OTP resend count should be an integer.", + empty: "Password recovery OTP resend count cannot be empty.", + range: + "Password recovery OTP resend count should be between 1 & 5.", + maxLengthReached: + "Password recovery OTP resend count should be a number with 1 digits." + } + }, + maxFailedAttemptCount: { + hint: "Password recovery maximum failed attempt count.", + label: "Max failed attempts count", + placeholder: "Enter max failed attempts", + validations: { + invalid: "Password recovery max failed attempts count should be an integer.", + empty: "Password recovery max failed attempts count cannot be empty.", + range: + "Password recovery max failed attempts count should be between 1 & 10.", + maxLengthReached: + "Password recovery max failed attempts count should be a number with less than 3 digits." + } + }, + smsOtpExpiryTime: { + hint: "Password recovery OTP expiry time in minutes.", + label: "Password recovery OTP expiry time", + placeholder: "Enter expiry time", + validations: { + invalid: "Password recovery OTP expiry time should be an integer.", + empty: "Password recovery OTP expiry time cannot be empty.", + range: + "Password recovery OTP expiry time should be between 1 minute & 1440 minutes " + + "(1 day).", + maxLengthReached: + "Password recovery OTP expiry time should be a number with 4 or less digits." + } + }, + passwordRecoveryOtpUseUppercase: { + hint: "This specifies whether to use upper case characters in the password recovery otp code.", + label: "Include upper case letters" + }, + passwordRecoveryOtpUseLowercase: { + hint: "This specifies whether to use lower case characters in the password recovery otp code.", + label: "Include lower case letters" + }, + passwordRecoveryOtpUseNumeric: { + hint: "This specifies whether to use numeric characters in the password recovery otp code.", + label: "Include numeric characters" + }, + passwordRecoveryOtpLength: { + hint: "Password recovery OTP length in characters", + label: "Password recovery OTP code length", + placeholder: "Enter OTP code length", + validations: { + empty: "Password recovery OTP length cannot be empty.", + maxLengthReached: + "Password recovery OTP length should be between 6 and 10 characters." + } + }, + enableEmailBasedRecovery: { + hint: "This specifies whether to send an recovery link to the email address.", + label: "Enable email link based recovery" } } }, + recoveryOptionSubHeadingEmailLink: "Email Link", + recoveryOptionSubHeadingSMS: "SMS OTP", + recoveryOptionHeading: "Recovery Option Selection", + otpConfigHeading: "OTP Code Configuration", + failedAttemptConfigHeading: "Recovery Attempts Limitation", connectorDescription: "Enable self-service password recovery for users " + "on the login page.", heading: "Password Recovery", notification: { @@ -3589,8 +3664,7 @@ export const extensions: Extensions = { } }, subHeading: - "Enable self-service password recovery for users " + - "on the login page.\nThe user will receive a password reset link via email upon request." + "Enable self-service password recovery for users on the login page." }, subHeading: "Account Recovery related settings." }, diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json index 5ba030bf3ff..331a6ace221 100644 --- a/apps/console/src/public/deployment.config.json +++ b/apps/console/src/public/deployment.config.json @@ -172,6 +172,7 @@ "appFaviconPath": "/assets/images/branding/favicon.ico", "isGOTEnabledForSuperTenantOnly": true, "showAppSwitchButton": true, + "showSmsOtpPwdRecoveryFeatureStatusChip": false, "features": { "administrators": { "disabledFeatures": [], diff --git a/features/admin.core.v1/configs/app.ts b/features/admin.core.v1/configs/app.ts index aa66311bb94..16cb7e30846 100644 --- a/features/admin.core.v1/configs/app.ts +++ b/features/admin.core.v1/configs/app.ts @@ -362,6 +362,8 @@ export class Config { productVersionConfig: window[ "AppUtils" ]?.getConfig()?.ui?.productVersionConfig, selfAppIdentifier: window[ "AppUtils" ]?.getConfig()?.ui?.selfAppIdentifier, showAppSwitchButton: window[ "AppUtils" ]?.getConfig()?.ui?.showAppSwitchButton, + showSmsOtpPwdRecoveryFeatureStatusChip: + window[ "AppUtils" ]?.getConfig()?.ui?.showSmsOtpPwdRecoveryFeatureStatusChip, systemAppsIdentifiers: window[ "AppUtils" ]?.getConfig()?.ui?.systemAppsIdentifiers, theme: window[ "AppUtils" ]?.getConfig()?.ui?.theme, useRoleClaimAsGroupClaim: window[ "AppUtils" ]?.getConfig()?.ui?.useRoleClaimAsGroupClaim diff --git a/features/admin.core.v1/models/config.ts b/features/admin.core.v1/models/config.ts index 87d2a72c1d6..b1347d72baf 100644 --- a/features/admin.core.v1/models/config.ts +++ b/features/admin.core.v1/models/config.ts @@ -450,6 +450,10 @@ export interface UIConfigInterface extends CommonUIConfigInterface void; - /** - * Is readonly. - */ - readOnly?: boolean; - /** - * Whether the connector is enabled using the toggle button. - */ - isConnectorEnabled?: boolean; - /** - * Specifies if the form is submitting. - */ - isSubmitting?: boolean; -} - -/** - * Form initial values interface. - */ -interface PasswordRecoveryFormInitialValuesInterface { - /** - * Recovery link expiry time. - */ - expiryTime: string; - /** - * Notify user on successful password recovery. - */ - notifySuccess: boolean; -} - -/** - * Proptypes for the Password Recovery Form error messages. - */ -export interface PasswordRecoveryFormErrorValidationsInterface { - /** - * Recovery link expiry time field. - */ - expiryTime: string; -} - -const allowedConnectorFields: string[] = [ - ServerConfigurationsConstants.NOTIFY_SUCCESS, - ServerConfigurationsConstants.RECOVERY_LINK_EXPIRY_TIME -]; - -const FORM_ID: string = "governance-connectors-password-recovery-form"; - -/** - * Password Recovery Configuration Form. - * - * @param props - Props injected to the component. - * @returns Functional component. - */ -export const PasswordRecoveryConfigurationForm: FunctionComponent = ( - props: PasswordRecoveryConfigurationFormPropsInterface -): ReactElement => { - - const { - initialValues, - onSubmit, - readOnly, - isConnectorEnabled, - isSubmitting, - ["data-testid"]: testId - } = props; - - const { t } = useTranslation(); - const [ initialConnectorValues, setInitialConnectorValues ] - = useState(undefined); - - /** - * Flattens and resolved form initial values and field metadata. - */ - useEffect(() => { - - if (isEmpty(initialValues?.properties)) { - return; - } - - let resolvedInitialValues: PasswordRecoveryFormInitialValuesInterface = null; - - initialValues.properties.map((property: ConnectorPropertyInterface) => { - if (allowedConnectorFields.includes(property.name)) { - if (property.name === ServerConfigurationsConstants.NOTIFY_SUCCESS) { - resolvedInitialValues = { - ...resolvedInitialValues, - notifySuccess: CommonUtils.parseBoolean(property.value) - }; - } else if (property.name === ServerConfigurationsConstants.RECOVERY_LINK_EXPIRY_TIME) { - resolvedInitialValues = { - ...resolvedInitialValues, - expiryTime: property.value - }; - } - } - }); - setInitialConnectorValues(resolvedInitialValues); - }, [ initialValues ]); - - /** - * Validate input data. - * - * @param values - Form values. - * @returns Form validation. - */ - const validateForm = (values: any): - PasswordRecoveryFormErrorValidationsInterface => { - - const errors: PasswordRecoveryFormErrorValidationsInterface = { - expiryTime: undefined - }; - - if (!values.expiryTime) { - // Check for required error. - errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.expiryTime.validations.empty"); - } else if (!FormValidation.isInteger(values.expiryTime as unknown as number)) { - // Check for invalid input. - errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.expiryTime.validations.invalid"); - } else if ((parseInt(values.expiryTime, 10) < GovernanceConnectorConstants - .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE) - || (parseInt(values.expiryTime, 10) > GovernanceConnectorConstants - .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE)) { - // Check for invalid range. - errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.expiryTime.validations.range"); - } else if (values.expiryTime && - !FormValidation.isLengthValid(values.expiryTime as string, GovernanceConnectorConstants - .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_LENGTH)) { - // Check for invalid input length. - errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.expiryTime.validations.maxLengthReached"); - } - - return errors; - }; - - /** - * Prepare form values for submitting. - * - * @param values - Form values. - * @returns Sanitized form values. - */ - const getUpdatedConfigurations = (values: Record) => { - const data: { - "Recovery.ExpiryTime": any; - "Recovery.NotifySuccess": boolean; - } = { - "Recovery.ExpiryTime": values.expiryTime !== undefined - ? values.expiryTime - : initialConnectorValues?.expiryTime, - "Recovery.NotifySuccess": values.notifySuccess !== undefined - ? !!values.notifySuccess - : initialConnectorValues?.notifySuccess - }; - - return data; - }; - - if (!initialConnectorValues) { - return null; - } - - return ( -
-
) => onSubmit(getUpdatedConfigurations(values)) } - validate={ validateForm } - uncontrolledForm={ false } - > - - - { - t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.notifySuccess.hint") - } - - - - - - { - t("extensions:manage.serverConfigurations.accountRecovery." + - "passwordRecovery.form.fields.expiryTime.hint") - } - -
- ); -}; - -/** - * Default props for the component. - */ -PasswordRecoveryConfigurationForm.defaultProps = { - "data-testid": "password-recovery-edit-form" -}; diff --git a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss new file mode 100644 index 00000000000..e7716035374 --- /dev/null +++ b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss @@ -0,0 +1,29 @@ +$form-level-2-margin-left: 50px; + +.connector-form.password-recovery-form { + h4.ui.header.heading { + margin-bottom: 35px; + } + + h5.ui.header.heading { + margin-bottom: 15px; + } + + .ui-hint { + margin-bottom: 25px; + } + + form > div > div:nth-child(5), + div:nth-child(6), + div:nth-child(7), + div:nth-child(8), + div:nth-child(12), + div:nth-child(13) { + margin-left: 50px; + } + + .MuiChip-root { + margin-left: 10px !important; + } + +} diff --git a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx new file mode 100644 index 00000000000..578ebc8d011 --- /dev/null +++ b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx @@ -0,0 +1,801 @@ +/** + * 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 Chip from "@oxygen-ui/react/Chip"; +import { TestableComponentInterface } from "@wso2is/core/models"; +import { CommonUtils } from "@wso2is/core/utils"; +import { Field, Form } from "@wso2is/form"; +import { Heading, Hint } from "@wso2is/react-components"; +import { FormValidation } from "@wso2is/validation"; +import isEmpty from "lodash-es/isEmpty"; +import React, { FunctionComponent, ReactElement, ReactNode, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { Divider, Label } from "semantic-ui-react"; +import { AppState } from "../../../admin.core.v1"; +import { GovernanceConnectorConstants } from "../../constants/governance-connector-constants"; +import { ServerConfigurationsConstants } from "../../constants/server-configurations-constants"; +import { + ConnectorPropertyInterface, + GovernanceConnectorInterface +} from "../../models/governance-connectors"; +import "./password-recovery-form.scss"; + +/** + * Interface for Password Recovery Configuration Form props. + */ +interface PasswordRecoveryConfigurationFormPropsInterface extends TestableComponentInterface { + /** + * Connector's initial values. + */ + initialValues: GovernanceConnectorInterface; + /** + * Callback for form submit. + * @param values - Resolved Form Values. + */ + onSubmit: (values) => void; + /** + * Is readonly. + */ + readOnly?: boolean; + /** + * Whether the connector is enabled using the toggle button. + */ + isConnectorEnabled?: boolean; + /** + * Specifies if the form is submitting. + */ + isSubmitting?: boolean; +} + +/** + * Form initial values interface. + */ +interface PasswordRecoveryFormInitialValuesInterface { + /** + * Recovery link expiry time. + */ + expiryTime: string; + /** + * Notify user on successful password recovery. + */ + notifySuccess: boolean; + /** + * Whether email based recovery is enabled. + */ + enableEmailBasedRecovery: boolean; + /** + * Whether SMS based recovery is enabled. + */ + enableSMSBasedRecovery: boolean; + /** + * SMS OTP expiry time. + */ + smsOtpExpiryTime: string; + /** + * Whether to use upper case letters in SMS OTP code. + */ + passwordRecoveryOtpUseUppercase: boolean; + /** + * Whether to use lower case letters in SMS OTP code. + */ + passwordRecoveryOtpUseLowercase: boolean; + /** + * Whether to use numeric characters in SMS OTP code. + */ + passwordRecoveryOtpUseNumeric: boolean; + /** + * The length of the SMS OTP code. + */ + smsOtpLength: string; + /** + * The maximum amount of times recovery code/link is resent. + */ + maxResendCount: string; + /** + * The maximum allowed failed attempts for a recovery flow. + */ + maxFailedAttemptCount: string; +} + +/** + * Proptypes for the Password Recovery Form error messages. + */ +export interface PasswordRecoveryFormErrorValidationsInterface { + /** + * Recovery link expiry time field. + */ + expiryTime: string; + /** + * Sms otp expiry time field + */ + smsOtpExpiryTime: string; + /** + * SMS OTP code length field. + */ + smsOtpLength: string; + /** + * max resend count field. + */ + maxResendCount: string; + /** + * Max allowed failed attempts count field. + */ + maxFailedAttemptCount: string; +} + +const allowedConnectorFields: string[] = [ + ServerConfigurationsConstants.NOTIFY_SUCCESS, + ServerConfigurationsConstants.RECOVERY_LINK_EXPIRY_TIME, + ServerConfigurationsConstants.RECOVERY_EMAIL_LINK_ENABLE, + ServerConfigurationsConstants.RECOVERY_SMS_OTP_ENABLE, + ServerConfigurationsConstants.RECOVERY_SMS_EXPIRY_TIME, + ServerConfigurationsConstants.RECOVERY_OTP_USE_UPPERCASE, + ServerConfigurationsConstants.RECOVERY_OTP_USE_LOWERCASE, + ServerConfigurationsConstants.RECOVERY_OTP_USE_NUMERIC, + ServerConfigurationsConstants.RECOVERY_OTP_LENGTH, + ServerConfigurationsConstants.RECOVERY_MAX_RESEND_COUNT, + ServerConfigurationsConstants.RECOVERY_MAX_FAILED_ATTEMPTS_COUNT +]; + +const FORM_ID: string = "governance-connectors-password-recovery-form"; + +/** + * Password Recovery Configuration Form. + * + * @param props - Props injected to the component. + * @returns Functional component. + */ +export const PasswordRecoveryConfigurationForm: FunctionComponent = ( + props: PasswordRecoveryConfigurationFormPropsInterface +): ReactElement => { + + const { + initialValues, + onSubmit, + readOnly, + isConnectorEnabled, + isSubmitting, + ["data-testid"]: testId + } = props; + + const { t } = useTranslation(); + const [ initialConnectorValues, setInitialConnectorValues ] + = useState(undefined); + const [ isEmailRecoveryEnabled, setIsEmailRecoveryEnabled ] = useState(false); + const [ isSMSRecoveryEnabled, setIsSMSRecoveryEnabled ] = useState(false); + const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState(false); + const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState(false); + const [ isNumericEnabled, setIsNumericEnabled ] = useState(false); + const showSmsOtpPasswordRecoveryBetaChip: boolean = + useSelector((state: AppState) => state?.config?.ui?.showSmsOtpPasswordRecoveryBetaChip); + + /** + * Flattens and resolved form initial values and field metadata. + */ + useEffect(() => { + if (isEmpty(initialValues?.properties)) { + return; + } + + let resolvedInitialValues: PasswordRecoveryFormInitialValuesInterface = null; + + initialValues.properties.map((property: ConnectorPropertyInterface) => { + if (allowedConnectorFields.includes(property.name)) { + if (property.name === ServerConfigurationsConstants.NOTIFY_SUCCESS) { + resolvedInitialValues = { + ...resolvedInitialValues, + notifySuccess: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_LINK_EXPIRY_TIME) { + resolvedInitialValues = { + ...resolvedInitialValues, + expiryTime: property.value + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_EMAIL_LINK_ENABLE) { + resolvedInitialValues = { + ...resolvedInitialValues, + enableEmailBasedRecovery: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_SMS_OTP_ENABLE) { + resolvedInitialValues = { + ...resolvedInitialValues, + enableSMSBasedRecovery: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_SMS_EXPIRY_TIME) { + resolvedInitialValues = { + ...resolvedInitialValues, + smsOtpExpiryTime: property.value + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_OTP_USE_UPPERCASE) { + resolvedInitialValues = { + ...resolvedInitialValues, + passwordRecoveryOtpUseUppercase: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_OTP_USE_LOWERCASE) { + resolvedInitialValues = { + ...resolvedInitialValues, + passwordRecoveryOtpUseLowercase: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_OTP_USE_NUMERIC) { + resolvedInitialValues = { + ...resolvedInitialValues, + passwordRecoveryOtpUseNumeric: CommonUtils.parseBoolean(property.value) + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_OTP_LENGTH) { + resolvedInitialValues = { + ...resolvedInitialValues, + smsOtpLength: property.value + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_MAX_RESEND_COUNT) { + resolvedInitialValues = { + ...resolvedInitialValues, + maxResendCount : property.value + }; + } else if (property.name === ServerConfigurationsConstants.RECOVERY_MAX_FAILED_ATTEMPTS_COUNT) { + resolvedInitialValues = { + ...resolvedInitialValues, + maxFailedAttemptCount : property.value + }; + } + } + }); + setInitialConnectorValues(resolvedInitialValues); + setIsEmailRecoveryEnabled(resolvedInitialValues?.enableEmailBasedRecovery); + setIsSMSRecoveryEnabled(resolvedInitialValues?.enableSMSBasedRecovery); + setIsUpperCaseEnabled(resolvedInitialValues?.passwordRecoveryOtpUseUppercase); + setIsLowerCaseEnabled(resolvedInitialValues?.passwordRecoveryOtpUseLowercase); + setIsNumericEnabled(resolvedInitialValues?.passwordRecoveryOtpUseNumeric); + }, [ initialValues ]); + + /** + * Validate input data. + * + * @param values - Form values. + * @returns Form validation. + */ + const validateForm = (values: any): + PasswordRecoveryFormErrorValidationsInterface => { + const errors: PasswordRecoveryFormErrorValidationsInterface = { + expiryTime: undefined, + maxFailedAttemptCount: undefined, + maxResendCount: undefined, + smsOtpExpiryTime: undefined, + smsOtpLength: undefined + }; + + if (!values.expiryTime) { + // Check for required error. + errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.expiryTime.validations.empty"); + } else if (!FormValidation.isInteger(values.expiryTime as unknown as number)) { + // Check for invalid input. + errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.expiryTime.validations.invalid"); + } else if ((parseInt(values.expiryTime, 10) < GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE) + || (parseInt(values.expiryTime, 10) > GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_VALUE)) { + // Check for invalid range. + errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.expiryTime.validations.range"); + } else if (values.smsOtpExpiryTime && + !FormValidation.isLengthValid(values.smsOtpExpiryTime as string, GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.SMS_OTP_EXPIRY_TIME_MAX_LENGTH)) { + // Check for invalid input length. + errors.smsOtpExpiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpExpiryTime.validations.maxLengthReached"); + } else if (values.expiryTime && + !FormValidation.isLengthValid(values.expiryTime as string, GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MAX_LENGTH)) { + // Check for invalid input length. + errors.expiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.expiryTime.validations.maxLengthReached"); + } else if (!values.smsOtpExpiryTime) { + // Check for required error. + errors.smsOtpExpiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpExpiryTime.validations.empty"); + } else if (!FormValidation.isInteger(values.smsOtpExpiryTime as unknown as number)) { + // Check for invalid input. + errors.smsOtpExpiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpExpiryTime.validations.invalid"); + } else if ((parseInt(values.smsOtpExpiryTime, 10) < GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.EXPIRY_TIME_MIN_VALUE) + || (parseInt(values.smsOtpExpiryTime, 10) > GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.SMS_OTP_EXPIRY_TIME_MAX_VALUE)) { + // Check for invalid range. + errors.smsOtpExpiryTime = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpExpiryTime.validations.range"); + } else if (!values.smsOtpLength) { + // Check for required error + errors.smsOtpLength = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpLength.validations.empty"); + } else if (parseInt(values.smsOtpLength, 10) < GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.SMS_OTP_CODE_LENGTH_MIN_VALUE || + parseInt(values.smsOtpLength, 10) > GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.SMS_OTP_CODE_LENGTH_MAX_VALUE) { + // Check for invalid input length. + errors.smsOtpLength = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpLength.validations.maxLengthReached"); + } else if (!values.maxResendCount) { + // Check for required error + errors.smsOtpLength = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxResendCount.validations.empty"); + } else if (parseInt(values.maxResendCount, 10) < GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.MAX_RESEND_COUNT_MIN_VALUE || + parseInt(values.maxResendCount, 10) > GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.MAX_RESEND_COUNT_MAX_VALUE) { + // Check for invalid range. + errors.maxResendCount = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxResendCount.validations.range"); + } else if (!values.maxFailedAttemptCount) { + // Check for required error + errors.maxFailedAttemptCount = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxFailedAttemptCount.validations.empty"); + } else if (parseInt(values.maxFailedAttemptCount, 10) < GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.MAX_FAILED_ATTEMPT_COUNT_MIN_VALUE || + parseInt(values.maxFailedAttemptCount, 10) > GovernanceConnectorConstants + .PASSWORD_RECOVERY_FORM_FIELD_CONSTRAINTS.MAX_FAILED_ATTEMPT_COUNT_MAX_VALUE) { + // Check for invalid range. + errors.maxFailedAttemptCount = t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxFailedAttemptCount.validations.range"); + } + + return errors; + }; + + /** + * Prepare form values for submitting. + * + * @param values - Form values. + * @returns Sanitized form values. + */ + const getUpdatedConfigurations = (values: Record) => { + const data: { + "Recovery.ExpiryTime": any; + "Recovery.Notification.Password.emailLink.Enable": boolean; + "Recovery.Notification.Password.ExpiryTime.smsOtp": number; + "Recovery.Notification.Password.MaxFailedAttempts": number; + "Recovery.Notification.Password.MaxResendAttempts": number; + "Recovery.Notification.Password.smsOtp.Enable": boolean; + "Recovery.Notification.Password.OTP.UseUppercaseCharactersInOTP": number; + "Recovery.Notification.Password.OTP.UseLowercaseCharactersInOTP": number; + "Recovery.Notification.Password.OTP.UseNumbersInOTP": number; + "Recovery.Notification.Password.OTP.OTPLength": number; + "Recovery.NotifySuccess": boolean; + } = { + "Recovery.ExpiryTime": values.expiryTime !== undefined + ? values.expiryTime + : initialConnectorValues?.expiryTime, + "Recovery.Notification.Password.ExpiryTime.smsOtp": values.smsOtpExpiryTime !== undefined + ? values.smsOtpExpiryTime + : initialConnectorValues?.smsOtpExpiryTime, + "Recovery.Notification.Password.MaxFailedAttempts": values.maxFailedAttemptCount !== undefined + ? values.maxFailedAttemptCount + : initialConnectorValues?.maxFailedAttemptCount, + "Recovery.Notification.Password.MaxResendAttempts": values.maxResendCount !== undefined + ? values.maxResendCount + : initialConnectorValues?.maxResendCount, + "Recovery.Notification.Password.OTP.OTPLength": values.smsOtpLength !== undefined + ? values.smsOtpLength + : initialConnectorValues?.smsOtpLength, + "Recovery.Notification.Password.OTP.UseLowercaseCharactersInOTP": + values.passwordRecoveryOtpUseLowercase !== undefined + ? values.passwordRecoveryOtpUseLowercase + : initialConnectorValues?.passwordRecoveryOtpUseLowercase, + "Recovery.Notification.Password.OTP.UseNumbersInOTP": values.passwordRecoveryOtpUseNumeric !== undefined + ? values.passwordRecoveryOtpUseNumeric + : initialConnectorValues?.passwordRecoveryOtpUseNumeric, + "Recovery.Notification.Password.OTP.UseUppercaseCharactersInOTP": + values.passwordRecoveryOtpUseUppercase !== undefined + ? values.passwordRecoveryOtpUseUppercase + : initialConnectorValues?.passwordRecoveryOtpUseUppercase, + "Recovery.Notification.Password.emailLink.Enable": values.enableEmailBasedRecovery !== undefined + ? !!values.enableEmailBasedRecovery + : initialConnectorValues?.enableEmailBasedRecovery, + "Recovery.Notification.Password.smsOtp.Enable": values.enableSMSBasedRecovery !== undefined + ? !!values.enableSMSBasedRecovery + : initialConnectorValues?.enableSMSBasedRecovery, + "Recovery.NotifySuccess": values.notifySuccess !== undefined + ? !!values.notifySuccess + : initialConnectorValues?.notifySuccess + }; + + return data; + }; + + if (!initialConnectorValues) { + return null; + } + + return ( +
+
) => onSubmit(getUpdatedConfigurations(values)) } + validate={ validateForm } + uncontrolledForm={ false } + > + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.recoveryOptionHeading") as ReactNode } + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.recoveryOptionSubHeadingEmailLink") as ReactNode } + + setIsEmailRecoveryEnabled(value) } + data-testid={ `${ testId }-email-link-based-recovery` } + /> + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode } + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.notifySuccess.hint") as ReactNode } + + + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.expiryTime.hint") as ReactNode } + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.recoveryOptionSubHeadingSMS") as ReactNode } + { + showSmsOtpPasswordRecoveryBetaChip ? + () + : <> + } + + setIsSMSRecoveryEnabled(value) } + data-testid={ `${ testId }-sms-based-recovery` } + /> + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.enableSMSBasedRecovery.hint") as ReactNode } + + + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.smsOtpExpiryTime.hint") as ReactNode } + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.otpConfigHeading") as ReactNode } + { + showSmsOtpPasswordRecoveryBetaChip ? + () + : <> + } + + setIsUpperCaseEnabled(value) } + data-testid={ `${ testId }-sms-otp-uppercase` } + /> + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.passwordRecoveryOtpUseUppercase.hint") as ReactNode } + + setIsLowerCaseEnabled(value) } + data-testid={ `${ testId }-sms-otp-lowercase` } + /> + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.passwordRecoveryOtpUseLowercase.hint") as ReactNode } + + setIsNumericEnabled(value) } + data-testid={ `${ testId }-sms-otp-numeric` } + /> + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.passwordRecoveryOtpUseNumeric.hint") as ReactNode } + + + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.passwordRecoveryOtpLength.hint") as ReactNode } + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.failedAttemptConfigHeading") as ReactNode } + { + showSmsOtpPasswordRecoveryBetaChip ? + () + : <> + } + + + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxFailedAttemptCount.hint") as ReactNode } + + + + + + { t("extensions:manage.serverConfigurations.accountRecovery." + + "passwordRecovery.form.fields.maxResendCount.hint") as ReactNode } + +
+ ); +}; + +/** + * Default props for the component. + */ +PasswordRecoveryConfigurationForm.defaultProps = { + "data-testid": "password-recovery-edit-form" +}; diff --git a/features/admin.server-configurations.v1/pages/connector-edit-page.tsx b/features/admin.server-configurations.v1/pages/connector-edit-page.tsx index 30c260a5afc..047d63954e8 100644 --- a/features/admin.server-configurations.v1/pages/connector-edit-page.tsx +++ b/features/admin.server-configurations.v1/pages/connector-edit-page.tsx @@ -41,8 +41,8 @@ import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Checkbox, CheckboxProps, Grid, Icon, Message, Ref } from "semantic-ui-react"; -import { serverConfigurationConfig } from "../../admin.extensions.v1/configs/server-configuration"; import { AppConstants, AppState, FeatureConfigInterface, history } from "../../admin.core.v1"; +import { serverConfigurationConfig } from "../../admin.extensions.v1/configs/server-configuration"; import { getConnectorDetails, updateGovernanceConnector } from "../api/governance-connectors"; import { ServerConfigurationsConstants } from "../constants/server-configurations-constants"; import { ConnectorFormFactory } from "../forms"; @@ -287,7 +287,10 @@ export const ConnectorEditPage: FunctionComponent = if ( serverConfigurationConfig.connectorToggleName[ connector?.name ] && - serverConfigurationConfig.autoEnableConnectorToggleProperty + serverConfigurationConfig.autoEnableConnectorToggleProperty && + // Recovery connector does not use a connector property. If either email link or + // sms-otp recovery is enabled, the connector is considered to be enabled. + connectorId !== ServerConfigurationsConstants.ACCOUNT_RECOVERY_CONNECTOR_ID ) { data.properties.push({ name: GovernanceConnectorUtils.decodeConnectorPropertyName( @@ -572,10 +575,6 @@ export const ConnectorEditPage: FunctionComponent = return ServerConfigurationsConstants.SELF_REGISTRATION_ENABLE; case ServerConfigurationsConstants.CAPTCHA_FOR_SSO_LOGIN_CONNECTOR_ID: return ServerConfigurationsConstants.RE_CAPTCHA_AFTER_MAX_FAILED_ATTEMPTS_ENABLE; - case ServerConfigurationsConstants.ACCOUNT_RECOVERY_CONNECTOR_ID: - return type === "username" - ? undefined - : ServerConfigurationsConstants.PASSWORD_RECOVERY_NOTIFICATION_BASED_ENABLE; case ServerConfigurationsConstants.ORGANIZATION_SELF_SERVICE_CONNECTOR_ID: return ServerConfigurationsConstants.ORGANIZATION_SELF_SERVICE_ENABLE; case ServerConfigurationsConstants.MULTI_ATTRIBUTE_LOGIN_CONNECTOR_ID: diff --git a/features/admin.server-configurations.v1/settings/edit-connector.tsx b/features/admin.server-configurations.v1/settings/edit-connector.tsx index 860ae7a56d1..6c6c015b5ca 100644 --- a/features/admin.server-configurations.v1/settings/edit-connector.tsx +++ b/features/admin.server-configurations.v1/settings/edit-connector.tsx @@ -20,8 +20,8 @@ import { TestableComponentInterface } from "@wso2is/core/models"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { SettingsSection } from "./settings-section"; -import { serverConfigurationConfig } from "../../admin.extensions.v1/configs"; import { AppConstants, history } from "../../admin.core.v1"; +import { serverConfigurationConfig } from "../../admin.extensions.v1/configs"; import { getSettingsSectionIcons } from "../configs"; import { ServerConfigurationsConstants } from "../constants/server-configurations-constants"; import { ConnectorPropertyInterface, GovernanceConnectorInterface } from "../models/governance-connectors"; @@ -172,7 +172,10 @@ export const EditConnector: FunctionComponent = ( header={ resolveConnectorTitle(connector) } onPrimaryActionClick={ handleSelection } primaryAction={ "Configure" } - connectorEnabled={ enableOption } + connectorEnabled={ connector?.id === ServerConfigurationsConstants.ACCOUNT_RECOVERY_CONNECTOR_ID + ? undefined + : enableOption + } > );