diff --git a/.changeset/wet-drinks-visit.md b/.changeset/wet-drinks-visit.md new file mode 100644 index 00000000000..618fbec3590 --- /dev/null +++ b/.changeset/wet-drinks-visit.md @@ -0,0 +1,9 @@ +--- +"@wso2is/admin.server-configurations.v1": minor +"@wso2is/admin.core.v1": minor +"@wso2is/theme": minor +"@wso2is/console": minor +"@wso2is/i18n": minor +--- + +Improve Sift configuration page diff --git a/.eslintrc.js b/.eslintrc.js index ae4acc63c00..9bf7f2808ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,7 +72,7 @@ const getLicenseHeaderPattern = () => { const LICENSE_HEADER_DEFAULT_PATTERN = [ "*", { - pattern: " Copyright \\(c\\) \\b(2019|202[0-6])(?:-(202[0-6]))?, WSO2 LLC. \\(https://www.wso2.com\\).$", + pattern: " Copyright \\(c\\) \\b(2019|202[0-6])(?:-(202[0-7]))?, WSO2 LLC. \\(https://www.wso2.com\\).$", template: " * Copyright (c) {{year}}, WSO2 LLC. (https://www.wso2.com)." }, " *", diff --git a/features/admin.core.v1/store/reducers/config.ts b/features/admin.core.v1/store/reducers/config.ts index 720f0da188a..ff7266badc3 100644 --- a/features/admin.core.v1/store/reducers/config.ts +++ b/features/admin.core.v1/store/reducers/config.ts @@ -123,6 +123,7 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< fidoConfigs: "", flow: "", flowMeta: "", + fraudDetectionConfigurations: "", getSecret: "", getSecretList: "", getSecretType: "", diff --git a/features/admin.server-configurations.v1/api/event-publishing-configurations.ts b/features/admin.server-configurations.v1/api/event-publishing-configurations.ts new file mode 100644 index 00000000000..8912bf0ff74 --- /dev/null +++ b/features/admin.server-configurations.v1/api/event-publishing-configurations.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2026, 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 { AsgardeoSPAClient, HttpClientInstance } from "@asgardeo/auth-react"; +import { RequestConfigInterface } from "@wso2is/admin.core.v1/hooks/use-request"; +import { store } from "@wso2is/admin.core.v1/store"; +import { IdentityAppsApiException } from "@wso2is/core/exceptions"; +import { HttpMethods } from "@wso2is/core/models"; +import { AxiosError, AxiosResponse } from "axios"; +import { ServerConfigurationsConstants } from "../constants/server-configurations-constants"; +import { FraudDetectionConfigurationsInterface } from "../models/fraud-detection"; + +/** + * Initialize an axios Http client. + * + */ +const httpClient: HttpClientInstance = AsgardeoSPAClient.getInstance().httpRequest.bind( + AsgardeoSPAClient.getInstance()); + +/** + * Function to update the Event Publishing Configurations. + */ +const updateEventPublishingConfigurations = ( + payload: FraudDetectionConfigurationsInterface +): Promise => { + const requestConfig: RequestConfigInterface = { + data: payload, + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.PUT, + url: store.getState().config.endpoints.fraudDetectionConfigurations + }; + + return httpClient(requestConfig) + .then((response: AxiosResponse) => { + if (response.status !== 200) { + throw new IdentityAppsApiException( + ServerConfigurationsConstants.CONFIGS_UPDATE_REQUEST_INVALID_STATUS_CODE_ERROR, + null, + response.status, + response.request, + response, + response.config); + } + + return Promise.resolve(response.data); + }) + .catch((error: AxiosError) => { + throw new IdentityAppsApiException( + ServerConfigurationsConstants.CONFIGS_UPDATE_REQUEST_ERROR, + error.stack, + error.code, + error.request, + error.response, + error.config); + }); +}; + +export default updateEventPublishingConfigurations; diff --git a/features/admin.server-configurations.v1/api/use-fraud-detection-configurations.ts b/features/admin.server-configurations.v1/api/use-fraud-detection-configurations.ts new file mode 100644 index 00000000000..5b3108ca301 --- /dev/null +++ b/features/admin.server-configurations.v1/api/use-fraud-detection-configurations.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025, 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 useRequest, { + RequestConfigInterface, + RequestErrorInterface, + RequestResultInterface +} from "@wso2is/admin.core.v1/hooks/use-request"; +import { store } from "@wso2is/admin.core.v1/store"; +import { HttpMethods } from "@wso2is/core/models"; +import { FraudDetectionConfigurationsInterface } from "../models/fraud-detection"; + +const useFraudDetectionConfigurations = ( + shouldFetch: boolean = true +): RequestResultInterface => { + const requestConfig: RequestConfigInterface = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + url: store.getState().config.endpoints.fraudDetectionConfigurations + }; + + const { data, error, isLoading, isValidating, mutate } = useRequest( + shouldFetch ? requestConfig : null + ); + + return { + data: data as Data, + error, + isLoading, + isValidating, + mutate + }; +}; + +export default useFraudDetectionConfigurations; diff --git a/features/admin.server-configurations.v1/configs/endpoints.ts b/features/admin.server-configurations.v1/configs/endpoints.ts index 244e7115dfb..2861b5b20dc 100644 --- a/features/admin.server-configurations.v1/configs/endpoints.ts +++ b/features/admin.server-configurations.v1/configs/endpoints.ts @@ -43,6 +43,7 @@ export const getServerConfigurationsResourceEndpoints = ( captchaForSSOLogin: `${ serverHost }/api/server/v1/identity-governance/${ ServerConfigurationsConstants.IDENTITY_GOVERNANCE_LOGIN_POLICIES_ID }/connectors/${ServerConfigurationsConstants.CAPTCHA_FOR_SSO_LOGIN_CONNECTOR_ID}`, + fraudDetectionConfigurations: `${ serverHost }/api/server/v1/configs/fraud-detection`, governanceConnectorCategories: `${ serverHost }/api/server/v1/identity-governance`, impersonationConfigurations: `${ serverHost }/api/server/v1/configs/impersonation`, loginPolicies: `${ serverHost }/api/server/v1/identity-governance/${ diff --git a/features/admin.server-configurations.v1/configs/ui.ts b/features/admin.server-configurations.v1/configs/ui.ts index 4cbffa655bc..5887c7c1831 100644 --- a/features/admin.server-configurations.v1/configs/ui.ts +++ b/features/admin.server-configurations.v1/configs/ui.ts @@ -30,6 +30,7 @@ import { VerticleFilterBarsIcon } from "@oxygen-ui/react-icons"; import UsernameValidationIcon from "@wso2is/admin.extensions.v1/assets/images/icons/username-validation-icon.svg"; +import { FunctionComponent, SVGProps } from "react"; import { default as LockRecoverIcon } from "../../themes/default/assets/images/icons/lock-recover-icon.svg"; @@ -93,6 +94,7 @@ import { import { default as JWTKeyIcon } from "../../themes/default/assets/images/illustrations/jwt-key-icon.svg"; +import { ReactComponent as SiftLogo } from "../../themes/wso2is/assets/images/connectors/sift.svg"; import { ServerConfigurationsConstants } from "../constants/server-configurations-constants"; interface GetGovernanceConnectorIllustrationsInterface { @@ -157,3 +159,14 @@ export const getConnectorCategoryIcon = (): ConnectorCategoryIconsInterface => { "default": GearIcon }; }; + +/** + * Get Sift connector icon. + */ +export const getSiftConnectorIcon = (): { + sift: FunctionComponent>; +} => { + return { + sift: SiftLogo + }; +}; diff --git a/features/admin.server-configurations.v1/constants/governance-connector-constants.ts b/features/admin.server-configurations.v1/constants/governance-connector-constants.ts index 14a2d7bcfaa..d708eb7d5fb 100644 --- a/features/admin.server-configurations.v1/constants/governance-connector-constants.ts +++ b/features/admin.server-configurations.v1/constants/governance-connector-constants.ts @@ -20,7 +20,8 @@ * Keys used in feature dictionary. */ export enum GovernanceConnectorFeatureDictionaryKeys { - HIDE_INVITED_USER_REGISTRATION_TOGGLE = "hideInvitedUserRegistrationToggle" + HIDE_INVITED_USER_REGISTRATION_TOGGLE = "hideInvitedUserRegistrationToggle", + HIDE_FRAUD_DETECTION_EVENT_PUBLISHING_CONFIGURATION = "hideFraudDetectionEventPublishingConfiguration" } /** @@ -35,7 +36,9 @@ export class GovernanceConnectorConstants { */ public static readonly featureDictionary: Record = { [GovernanceConnectorFeatureDictionaryKeys.HIDE_INVITED_USER_REGISTRATION_TOGGLE]: - "governanceConnectors.invitedUserRegistration.enableDisableControl" + "governanceConnectors.invitedUserRegistration.enableDisableControl", + [GovernanceConnectorFeatureDictionaryKeys.HIDE_FRAUD_DETECTION_EVENT_PUBLISHING_CONFIGURATION]: + "governanceConnectors.fraudDetection.eventPublishingConfigurations" }; /** diff --git a/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.scss b/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.scss index fca07fccddf..5ad5baddd53 100644 --- a/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.scss +++ b/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.scss @@ -16,7 +16,7 @@ * under the License. */ -.sift-connector-form{ +.sift-connector-form { .oxygen-text-field { min-width: 350px; } diff --git a/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.tsx b/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.tsx index cfea6a6b094..9c9726f5df4 100644 --- a/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.tsx +++ b/features/admin.server-configurations.v1/forms/sift-connector-form/sift-connector-form.tsx @@ -16,17 +16,39 @@ * under the License. */ +import { Grid, Stack } from "@mui/material"; +import Alert from "@oxygen-ui/react/Alert"; +import Box from "@oxygen-ui/react/Box"; +import FormGroup from "@oxygen-ui/react/FormGroup"; import IconButton from "@oxygen-ui/react/IconButton"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; -import Stack from "@oxygen-ui/react/Stack"; +import Typography from "@oxygen-ui/react/Typography"; import { TrashIcon } from "@oxygen-ui/react-icons"; -import { IdentifiableComponentInterface } from "@wso2is/core/models"; -import { FinalForm, FinalFormField, FormRenderProps, TextFieldAdapter } from "@wso2is/form"; -import { PrimaryButton } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, useState } from "react"; +import { AppState } from "@wso2is/admin.core.v1/store"; +import { isFeatureEnabled } from "@wso2is/core/helpers"; +import { AlertLevels, FeatureAccessConfigInterface, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { FinalForm, FinalFormField, FormApi, FormRenderProps, TextFieldAdapter } from "@wso2is/form"; +import CheckboxAdapter from "@wso2is/form/src/components/adapters/checkbox-field-adapter"; +import { ContentLoader, GenericIcon, PrimaryButton } from "@wso2is/react-components"; +import React, { FunctionComponent, ReactElement, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Icon } from "semantic-ui-react"; +import { useDispatch, useSelector } from "react-redux"; +import { Dispatch } from "redux"; +import { Divider, Icon } from "semantic-ui-react"; +import updateEventPublishingConfigurations from "../../api/event-publishing-configurations"; +import useFraudDetectionConfigurations from "../../api/use-fraud-detection-configurations"; +import { getSiftConnectorIcon } from "../../configs/ui"; +import { + GovernanceConnectorConstants, + GovernanceConnectorFeatureDictionaryKeys +} from "../../constants/governance-connector-constants"; import { ServerConfigurationsConstants } from "../../constants/server-configurations-constants"; +import { + FraudAnalyticEventMetadataInterface, + FraudAnalyticEventPropertyInterface, + FraudDetectionConfigurationsInterface +} from "../../models/fraud-detection"; import { GovernanceConnectorInterface } from "../../models/governance-connectors"; import { GovernanceConnectorUtils } from "../../utils/governance-connector-utils"; import "./sift-connector-form.scss"; @@ -69,9 +91,65 @@ const SiftConnectorForm: FunctionComponent = ({ }: SiftConnectorFormPropsInterface): ReactElement => { const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); - const [ apiKey, setApiKey ] = useState(initialValues?.properties[0]?.value ?? ""); - const [ isShow, setIsShow ] = useState(false); + const governanceConnectorsFeatureConfig: FeatureAccessConfigInterface = useSelector( + (state: AppState) => state?.config?.ui?.features?.governanceConnectors + ); + const showFraudDetectionEventPublishingConfiguration: boolean = isFeatureEnabled( + governanceConnectorsFeatureConfig, + GovernanceConnectorConstants.featureDictionary[ + GovernanceConnectorFeatureDictionaryKeys.HIDE_FRAUD_DETECTION_EVENT_PUBLISHING_CONFIGURATION + ] + ); + + const [ isShow, setIsShow ] = useState(false); + + const { + data: fraudDetectionConfigurations, + error: fraudDetectionConfigurationsError, + isLoading: isFraudDetectionConfigurationsLoading, + mutate: mutateEventPublishingConfigurations + } = useFraudDetectionConfigurations(showFraudDetectionEventPublishingConfiguration); + + const eventMetadata: Record = GovernanceConnectorUtils + .resolveEventPropertyMappings(); + + // Memoize enriched fraud detection configurations + const enrichedFraudDetectionConfigurations: FraudDetectionConfigurationsInterface | undefined = useMemo(() => { + if (!fraudDetectionConfigurations) return undefined; + + return { + ...fraudDetectionConfigurations, + events: fraudDetectionConfigurations.events.map((event: FraudAnalyticEventPropertyInterface) => ({ + ...event, + description: eventMetadata[event.eventName]?.description || "", + displayName: eventMetadata[event.eventName]?.displayName || event.eventName + })) + }; + }, [ fraudDetectionConfigurations, eventMetadata ]); + + // Split events into two columns + const { leftColumnEvents, rightColumnEvents } = useMemo(() => { + if (!enrichedFraudDetectionConfigurations?.events) { + return { leftColumnEvents: [], rightColumnEvents: [] }; + } + + enrichedFraudDetectionConfigurations.events.sort( + (a: FraudAnalyticEventPropertyInterface, b: FraudAnalyticEventPropertyInterface) => { + const orderA: number = eventMetadata[a.eventName]?.displayOrder || 0; + const orderB: number = eventMetadata[b.eventName]?.displayOrder || 0; + + return orderA - orderB; + } + ); + const midPoint: number = Math.ceil(enrichedFraudDetectionConfigurations.events.length / 2); + + return { + leftColumnEvents: enrichedFraudDetectionConfigurations.events.slice(0, midPoint), + rightColumnEvents: enrichedFraudDetectionConfigurations.events.slice(midPoint) + }; + }, [ enrichedFraudDetectionConfigurations ]); /** * Get updated API Key. @@ -81,20 +159,13 @@ const SiftConnectorForm: FunctionComponent = ({ */ const getUpdatedAPIKey = (values: Record) => { - let data: { [ key: string]: unknown } = { - [ ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY ]: "" - }; - - if (Object.keys(values).length === 0) { - return data; - } + const encodedApiKeyProperty: string = GovernanceConnectorUtils + .encodeConnectorPropertyName(ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY); - for (const key in values) { - data = { - ...data, - [ GovernanceConnectorUtils.decodeConnectorPropertyName(key) ]: values[ key ] - }; - } + const data: { [ key: string]: unknown } = { + [ ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY ]: + values[encodedApiKeyProperty] || "" + }; return data; }; @@ -118,93 +189,383 @@ const SiftConnectorForm: FunctionComponent = ({ ); + /** + * Render event selection checkbox. + * + * @param event - Fraud analytic event property. + * @returns ReactElement + */ + const renderEventSelectionCheckbox = (event: FraudAnalyticEventPropertyInterface): ReactElement => { + return ( + + + + + { t(event.displayName) } + + + + { t(event.description) } + + ) + } + /> + + ); + }; + + /** + * Render event publishing section. + * + * @returns ReactElement + */ + const renderEventPublishingSection = (form: FormApi>): ReactElement => { + return ( + <> + + { + (form.getState().values.publishDeviceMetadata && + form.getState().values.publishUserInfo) && ( + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity.connectors" + + ".siftConnector.eventPublishing.eventProperties.piiPublishingWarning") } + + ) + } + + + + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishUserInfo.label") } + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishUserInfo.description") } + + ) + } + /> + { + (form.getState().values.publishUserInfo && + !form.getState().values.publishDeviceMetadata) && ( + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishUserInfo.warning") } + + ) + } + + + + + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishDeviceMetadata.label") } + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishDeviceMetadata.description") } + + ) + } + /> + { + (form.getState().values.publishDeviceMetadata && + !form.getState().values.publishUserInfo) && ( + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties" + + ".publishDeviceMetadata.warning") } + + ) + } + + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity.connectors." + + "siftConnector.eventPublishing.eventProperties.title") } + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity.connectors." + + "siftConnector.eventPublishing.eventProperties.subtitle") } + + + + + + { leftColumnEvents?.map((event: FraudAnalyticEventPropertyInterface) => + renderEventSelectionCheckbox(event)) } + + + + + { rightColumnEvents?.map( + (event: FraudAnalyticEventPropertyInterface) => + renderEventSelectionCheckbox(event) + ) } + + + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity.connectors." + + "siftConnector.eventPublishing.eventDiagnostics.title") } + + + + + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventDiagnostics." + + "logRequestPayload.label") } + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventDiagnostics." + + "logRequestPayload.description") } + + ) + } + /> + + + + ); + }; + + /** + * Handle form submission for event publishing configurations. + * + * @param values - Form values. + */ + const handleEventPublishingSubmit = (values: Record) => { + const eventPublishingPayload: FraudDetectionConfigurationsInterface = { + events: enrichedFraudDetectionConfigurations?.events.map((event: FraudAnalyticEventPropertyInterface) => ({ + enabled: values.events?.[ GovernanceConnectorUtils + .encodeConnectorPropertyName(event.eventName)] as boolean, + eventName: event.eventName, + properties: [] + })) || [], + logRequestPayload: values.logRequestPayload as boolean || false, + publishDeviceMetadata: values.publishDeviceMetadata as boolean || false, + publishUserInfo: values.publishUserInfo as boolean || false + }; + + updateEventPublishingConfigurations(eventPublishingPayload).then(() => { + if (values[GovernanceConnectorUtils.encodeConnectorPropertyName( + ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY + )] !== undefined) { + onSubmit(getUpdatedAPIKey(values)); + } + + mutateEventPublishingConfigurations(); + }).catch(() => { + dispatch( + addAlert({ + description: t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.notifications.eventPropertiesUpdate.error.description"), + level: AlertLevels.ERROR, + message: t("governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.notifications.eventPropertiesUpdate.error.message") + }) + ); + }); + }; + + /** * Resolve initial form values. + * + * @returns Initial form values. + */ + const resolveInitialFormValues = () => { + return { + [ GovernanceConnectorUtils + .encodeConnectorPropertyName(ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY) + ]: initialValues?.properties[0]?.value ?? "", + events: enrichedFraudDetectionConfigurations?.events.reduce( + (formValue: Record, event: FraudAnalyticEventPropertyInterface) => { + + formValue[GovernanceConnectorUtils.encodeConnectorPropertyName(event.eventName)] = event.enabled; + + return formValue; + }, {} as Record) ?? {}, + logRequestPayload: enrichedFraudDetectionConfigurations?.logRequestPayload ?? false, + publishDeviceMetadata: enrichedFraudDetectionConfigurations?.publishDeviceMetadata ?? false, + publishUserInfo: enrichedFraudDetectionConfigurations?.publishUserInfo ?? false + }; + }; + return (
) => - onSubmit(getUpdatedAPIKey(values)) - } - data-componentid={ `${ componentId }-configuration-form` } - initialValues={ - { - [ GovernanceConnectorUtils - .encodeConnectorPropertyName(ServerConfigurationsConstants.SIFT_CONNECTOR_API_KEY_PROPERTY) - ]: initialValues?.properties[0]?.value ?? "" + onSubmit={ (values: Record) => { + if (!showFraudDetectionEventPublishingConfiguration) { + // Only submit API key + onSubmit(getUpdatedAPIKey(values)); + } else { + // Submit event publishing configurations + handleEventPublishingSubmit(values); } - } + } } + data-componentid={ `${ componentId }-configuration-form` } + initialValues={ resolveInitialFormValues() } render={ ({ handleSubmit, form }: FormRenderProps) => ( -
- - + + + + { t("governanceConnectors:connectorCategories.loginAttemptsSecurity.connectors" + + ".siftConnector.title") } + + + + + + { + form.getState().values[GovernanceConnectorUtils .encodeConnectorPropertyName(ServerConfigurationsConstants - .SIFT_CONNECTOR_API_KEY_PROPERTY) - } - inputType="password" - type={ isShow ? "text" : "password" } - label={ t("governanceConnectors:connectorCategories" + - ".loginAttemptsSecurity.connectors.siftConnector" + - ".properties.siftConnectorApiKey.label") + .SIFT_CONNECTOR_API_KEY_PROPERTY)] && ( + { + form.change( + GovernanceConnectorUtils + .encodeConnectorPropertyName(ServerConfigurationsConstants + .SIFT_CONNECTOR_API_KEY_PROPERTY), "" + ); + } } + data-componentid={ + `${ componentId }-configuration-form-api-key-delete-button` + } + > + + + ) } - placeholder={ t("governanceConnectors:connectorCategories" + - ".loginAttemptsSecurity.connectors.siftConnector" + - ".properties.siftConnectorApiKey.placeholder") - } - component={ TextFieldAdapter } - autoComplete="new-password" - InputProps={ { - endAdornment: renderInputAdornment() - } } - initialValue={ initialValues?.properties[0]?.value ?? "" } - onChange={ (e: React.ChangeEvent) => { - const value: string = e.target.value; - - setApiKey(value); - form.change(GovernanceConnectorUtils - .encodeConnectorPropertyName(ServerConfigurationsConstants - .SIFT_CONNECTOR_API_KEY_PROPERTY), value - ); - } } - value={ apiKey } - /> + { - (apiKey && apiKey !== "") && ( - { - setApiKey(""); - form.change( - GovernanceConnectorUtils - .encodeConnectorPropertyName(ServerConfigurationsConstants - .SIFT_CONNECTOR_API_KEY_PROPERTY), "" - ); - } } - data-componentid={ `${ componentId }-configuration-form-api-key-delete-button` } - > - - - ) - } - - + : showFraudDetectionEventPublishingConfiguration + && ( + <> + + + + { t("governanceConnectors:connectorCategories" + + ".loginAttemptsSecurity.connectors.siftConnector" + + ".eventPublishing.title") } + + + { t("governanceConnectors:connectorCategories" + + ".loginAttemptsSecurity.connectors.siftConnector" + + ".eventPublishing.subtitle") } + + { renderEventPublishingSection(form) } + + + ) + } - > - { t("common:update") } - -
+ + { t("common:update") } + + + ) } />
diff --git a/features/admin.server-configurations.v1/models/endpoints.ts b/features/admin.server-configurations.v1/models/endpoints.ts index 784a086e008..fcbf4b9538e 100644 --- a/features/admin.server-configurations.v1/models/endpoints.ts +++ b/features/admin.server-configurations.v1/models/endpoints.ts @@ -40,4 +40,5 @@ export interface ServerConfigurationsResourceEndpointsInterface { selfSignUp: string; serverConfigurations: string; serverSupportedSchemas: string; + fraudDetectionConfigurations: string; } diff --git a/features/admin.server-configurations.v1/models/fraud-detection.ts b/features/admin.server-configurations.v1/models/fraud-detection.ts new file mode 100644 index 00000000000..607519d9277 --- /dev/null +++ b/features/admin.server-configurations.v1/models/fraud-detection.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025, 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. + */ + +/** + * Interface for the Fraud Detection Configurations. + */ +export interface FraudDetectionConfigurationsInterface { + logRequestPayload: boolean; + publishUserInfo: boolean; + publishDeviceMetadata: boolean; + events: FraudAnalyticEventPropertyInterface[]; +} + +/** + * Interface for the Fraud Analytic Event Property. + */ +export interface FraudAnalyticEventPropertyInterface { + eventName: string; + enabled: boolean; + properties: string[]; + displayName?: string; + description?: string; + displayOrder?: number; +} + +/** + * Interface for the Fraud Analytic Event Metadata. + */ +export interface FraudAnalyticEventMetadataInterface { + displayName: string; + description: string; + displayOrder: number; +} diff --git a/features/admin.server-configurations.v1/utils/governance-connector-utils.ts b/features/admin.server-configurations.v1/utils/governance-connector-utils.ts index 7a276d015d7..7be5982c528 100644 --- a/features/admin.server-configurations.v1/utils/governance-connector-utils.ts +++ b/features/admin.server-configurations.v1/utils/governance-connector-utils.ts @@ -28,6 +28,7 @@ import { I18n } from "@wso2is/i18n"; import camelCase from "lodash-es/camelCase"; import { getConnectorCategories } from "../api/governance-connectors"; import { ServerConfigurationsConstants } from "../constants/server-configurations-constants"; +import { FraudAnalyticEventMetadataInterface } from "../models/fraud-detection"; import { ConnectorOverrideConfig, ConnectorPropertyInterface, @@ -588,4 +589,57 @@ export class GovernanceConnectorUtils { .properties; } + + /** + * Resolve event property mappings. + * + * @returns Event property mappings. + */ + public static resolveEventPropertyMappings(): Record { + + return { + "Event.Notification_Based_Verification": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.userVerifications.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.userVerifications.label", + displayOrder: 6 + }, + "Event.User_Credential_Update": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.credentialUpdates.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.credentialUpdates.label", + displayOrder: 4 + }, + "Event.User_Login": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.logins.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.logins.label", + displayOrder: 2 + }, + "Event.User_Logout": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.logouts.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.logouts.label", + displayOrder: 3 + }, + "Event.User_Profile_Update": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.userProfileUpdates.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.userProfileUpdates.label", + displayOrder: 5 + }, + "Event.User_Registration": { + description: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.registrations.hint", + displayName: "governanceConnectors:connectorCategories.loginAttemptsSecurity" + + ".connectors.siftConnector.eventPublishing.eventProperties.events.registrations.label", + displayOrder: 1 + } + }; + } } diff --git a/modules/i18n/src/models/namespaces/governance-connectors-ns.ts b/modules/i18n/src/models/namespaces/governance-connectors-ns.ts index 2c79b588c3a..36ebfa84f17 100644 --- a/modules/i18n/src/models/namespaces/governance-connectors-ns.ts +++ b/modules/i18n/src/models/namespaces/governance-connectors-ns.ts @@ -338,6 +338,7 @@ export interface governanceConnectorsNS { }; }; siftConnector: { + title: string; properties: { name: string; description: string; @@ -346,9 +347,60 @@ export interface governanceConnectorsNS { placeholder: string; }; }; + eventPublishing: { + title: string; + subtitle: string; + eventProperties: { + title: string; + subtitle: string; + piiPublishingWarning: string; + publishDeviceMetadata: { + label: string; + description: string; + warning: string; + }; + publishUserInfo: { + label: string; + description: string; + warning: string; + }; + events: { + logins: { + label: string; + hint: string; + }; + logouts: { + label: string; + hint: string; + }; + registrations: { + label: string; + hint: string; + }; + credentialUpdates: { + label: string; + hint: string; + }; + userProfileUpdates: { + label: string; + hint: string; + }; + userVerifications: { + label: string; + hint: string; + }; + }; + }; + eventDiagnostics: { + title: string; + logRequestPayload: { + label: string; + description: string; + }; + }; + }; notifications: { - configurationUpdate: { - success: NotificationItem; + eventPropertiesUpdate: { error: NotificationItem; }; }; diff --git a/modules/i18n/src/translations/en-US/portals/governance-connectors.ts b/modules/i18n/src/translations/en-US/portals/governance-connectors.ts index 9c69ca4189e..46cf4b44f4d 100644 --- a/modules/i18n/src/translations/en-US/portals/governance-connectors.ts +++ b/modules/i18n/src/translations/en-US/portals/governance-connectors.ts @@ -276,6 +276,62 @@ export const governanceConnectors: governanceConnectorsNS = { } }, siftConnector: { + title: "Sift Integration", + eventPublishing: { + title: "Configure Event Publishing", + subtitle: "Choose which user activities to publish to Sift and what data to include in the payload.", + eventProperties: { + title: "Events to Publish", + subtitle: "Select the events you want to publish to Sift for analysis.", + piiPublishingWarning: "You’ve enabled sending both user profile and device data (e.g., email, name, IP address) to Sift. " + + "Ensure this complies with your organization's privacy policy.", + publishDeviceMetadata: { + label: "Include user device metadata in the event payload", + description: "Publish user device metadata like IP address and user agent.", + warning: "This will send IP addresses and user agents, which may be considered personal data. " + + "Ensure this complies with your organization's privacy policy and data-sharing agreements." + }, + publishUserInfo: { + label: "Include user profile information in the event payload", + description: "Publish user attributes like username, email, mobile, etc.", + warning: "This will send sensitive, direct user identifiers to Sift. " + + "Ensure this complies with your organization's privacy policy and data-sharing agreements." + }, + events: { + logins: { + label: "Logins", + hint: "Publish user login events." + }, + logouts: { + label: "Logouts", + hint: "Publish user logout events." + }, + registrations: { + label: "Registrations", + hint: "Publish user registration events." + }, + credentialUpdates: { + label: "Credential Updates", + hint: "Publish user credential update events." + }, + userProfileUpdates: { + label: "User Profile Updates", + hint: "Publish user profile update events." + }, + userVerifications: { + label: "User Verifications", + hint: "Publish notification based user verification events." + } + } + }, + eventDiagnostics: { + title: "Diagnostic Logging", + logRequestPayload: { + label: "Log event payloads locally", + description: "Save a copy of each event payload to local diagnostic logs for debugging purposes." + } + } + }, properties: { name: "Fraud Detection", description: "Integrate Sift to detect and prevent fraudulent account logins.", @@ -285,14 +341,10 @@ export const governanceConnectors: governanceConnectorsNS = { } }, notifications: { - configurationUpdate: { + eventPropertiesUpdate: { error: { - description: "An error occurred while updating the Sift configuration.", - message: "Update Error" - }, - success: { - description: "Successfully updated the Sift configuration.", - message: "Update Successful" + description: "An error occurred while updating the event publishing configurations.", + message: "Event Configurations Update Error" } } } diff --git a/modules/theme/src/themes/wso2is/assets/images/connectors/sift.svg b/modules/theme/src/themes/wso2is/assets/images/connectors/sift.svg new file mode 100644 index 00000000000..d7b46add63c --- /dev/null +++ b/modules/theme/src/themes/wso2is/assets/images/connectors/sift.svg @@ -0,0 +1,26 @@ + + + + +