diff --git a/.changeset/lovely-birds-run.md b/.changeset/lovely-birds-run.md new file mode 100644 index 00000000000..ea4a14a8770 --- /dev/null +++ b/.changeset/lovely-birds-run.md @@ -0,0 +1,5 @@ +--- +"@wso2is/admin.remote-userstores.v1": minor +--- + +[feat] introduce new remote user store impl - add file verification steps diff --git a/features/admin.remote-userstores.v1/api/use-get-sha-file.ts b/features/admin.remote-userstores.v1/api/use-get-sha-file.ts new file mode 100644 index 00000000000..c89767d6a06 --- /dev/null +++ b/features/admin.remote-userstores.v1/api/use-get-sha-file.ts @@ -0,0 +1,70 @@ +/** + * 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 useRequest, { + RequestConfigInterface, + RequestErrorInterface, + RequestResultInterface +} from "@wso2is/admin.core.v1/hooks/use-request"; +import { HttpMethods } from "@wso2is/core/models"; +import isEmpty from "lodash-es/isEmpty"; + +/** + * Hook to get the checksum file content. + * + * @param filePath - File path to be fetched. + * @param shouldFetch - If true, will fetch the data. + * + * @returns content of the file. + */ +const useGetCheckSum = ( + filePath: string, + shouldFetch: boolean = true +): RequestResultInterface => { + + const requestConfig: RequestConfigInterface = { + headers: { + "Accept": "text/plain", + "Content-Type": "text/plain" + }, + method: HttpMethods.GET, + responseType: "text", + url: filePath + }; + + const { + data, + error, + isLoading, + isValidating, + mutate + } = useRequest( + (shouldFetch && !isEmpty(filePath)) ? requestConfig : null, + { attachToken: false } + ); + + return { + data, + error, + isLoading, + isValidating, + mutate + }; +}; + +export default useGetCheckSum; diff --git a/features/admin.remote-userstores.v1/components/edit/setup-guide/remote/download-step.tsx b/features/admin.remote-userstores.v1/components/edit/setup-guide/remote/download-step.tsx index 6adbea46234..45989158fee 100644 --- a/features/admin.remote-userstores.v1/components/edit/setup-guide/remote/download-step.tsx +++ b/features/admin.remote-userstores.v1/components/edit/setup-guide/remote/download-step.tsx @@ -17,12 +17,46 @@ */ import Button from "@oxygen-ui/react/Button"; +import Code from "@oxygen-ui/react/Code"; +import List from "@oxygen-ui/react/List"; +import ListItem from "@oxygen-ui/react/ListItem"; +import Skeleton from "@oxygen-ui/react/Skeleton"; import Stack from "@oxygen-ui/react/Stack"; import Typography from "@oxygen-ui/react/Typography"; import { DownloadIcon } from "@oxygen-ui/react-icons"; -import { IdentifiableComponentInterface } from "@wso2is/core/models"; -import React, { FunctionComponent, ReactElement } from "react"; -import { useTranslation } from "react-i18next"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { CodeEditor, Heading } from "@wso2is/react-components"; +import isEmpty from "lodash-es/isEmpty"; +import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import useGetCheckSum from "../../../../api/use-get-sha-file"; + +/** + * Interface for download URLs prop of the DownloadAgentStep component. + */ +export interface AgentDownloadInfoInterface { + /** + * The download URL of the user store agent. + */ + file: string; + /** + * The checksum file URL of the user store agent file. + */ + checkSum: string; +} + +/** + * Interface for agent download options shown it the UI. + */ +interface AgentDownloadOptionInterface extends AgentDownloadInfoInterface { + /** + * The operating system of the user store agent. + */ + os: OperatingSystem; +} /** * Download agent step component props. @@ -32,11 +66,21 @@ interface DownloadAgentStepPropsInterface extends IdentifiableComponentInterface * Download URLs of the user store agent for different operating systems. */ downloadURLs: { - mac: string; - linux: string; - linuxArm: string; - windows: string; - }; + linux: AgentDownloadInfoInterface; + linuxArm: AgentDownloadInfoInterface; + mac: AgentDownloadInfoInterface; + windows: AgentDownloadInfoInterface; + } +} + +/** + * User store agent available operating systems. + */ +enum OperatingSystem { + Linux = "Linux", + LinuxArm = "Linux (ARM)", + Mac = "Mac OS", + Windows = "Windows" } /** @@ -50,37 +94,103 @@ const DownloadAgentStep: FunctionComponent = ({ ["data-componentid"]: componentId = "download-agent-step" }: DownloadAgentStepPropsInterface): ReactElement => { const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ downloadedOS, setDownloadedOS ] = useState(); - const availableOptions: { os: string; link: string }[] = [ + const availableOptions: AgentDownloadOptionInterface[] = [ { - link: downloadURLs?.linux ?? "", - os: "Linux" + checkSum: downloadURLs?.linux?.checkSum ?? "", + file: downloadURLs?.linux?.file ?? "", + os: OperatingSystem.Linux }, { - link: downloadURLs?.linuxArm ?? "", - os: "Linux (ARM)" + checkSum: downloadURLs?.linuxArm?.checkSum ?? "", + file: downloadURLs?.linuxArm?.file ?? "", + os: OperatingSystem.LinuxArm }, { - link: downloadURLs?.mac ?? "", - os: "Mac OS" + checkSum: downloadURLs?.mac?.checkSum ?? "", + file: downloadURLs?.mac?.file ?? "", + os: OperatingSystem.Mac }, { - link: downloadURLs?.windows ?? "", - os: "Windows" + checkSum: downloadURLs?.windows?.checkSum ?? "", + file: downloadURLs?.windows?.file ?? "", + os: OperatingSystem.Windows } ]; - const onDownloadClicked = (downloadURL: string) => { + /** + * Resolves the selected check sum path based on the downloaded agent's OS. + */ + const selectedCheckSumPath: string = useMemo(() => { + return availableOptions.find( + (option: (AgentDownloadInfoInterface & { os: OperatingSystem })) => option.os === downloadedOS)?.checkSum; + }, [ downloadedOS ]); + + const { + data: fetchedCheckSumContent, + error: checkSumFetchRequestError, + isLoading: isCheckSumFetchRequestLoading, + isValidating: isCheckSumFetchRequestValidating + } = useGetCheckSum(selectedCheckSumPath, !!downloadedOS); + + const checkSum: string = fetchedCheckSumContent?.split(" ")[0] ?? ""; + + /** + * Handles the check sum fetch error. + */ + useEffect(() => { + if (checkSumFetchRequestError) { + dispatch(addAlert({ + description: t("remoteUserStores:notifications.checkSumError.description"), + level: AlertLevels.SUCCESS, + message: t("remoteUserStores:notifications.checkSumError.message") + })); + } + }, [ checkSumFetchRequestError ]); + + const onDownloadClicked = (os: OperatingSystem, downloadURL: string) => { window.open(downloadURL, "_blank", "noopener, noreferrer"); + setDownloadedOS(os); + }; + + const renderLoadingPlaceholder = () => { + return ( +
+ + + +
+ ); + }; + + /** + * Resolves the verification command based on the downloaded agent's OS. + * @returns The verification command. + */ + const getVerificationCommand = () => { + switch(downloadedOS) { + case OperatingSystem.Linux: + case OperatingSystem.LinuxArm: + return "sha256sum "; + case OperatingSystem.Mac: + return "shasum -a 256 "; + case OperatingSystem.Windows: + return "certutil -hashFile SHA256"; + default: + return ""; + } }; return ( - <> +
{ t("remoteUserStores:pages.edit.guide.steps.download.remote.description") } - { availableOptions.map((option: { os: string; link: string }) => { + { availableOptions.map((option: AgentDownloadOptionInterface) => { return ( ); }) } - + { (isCheckSumFetchRequestLoading || isCheckSumFetchRequestValidating) && renderLoadingPlaceholder() } + { + !(isCheckSumFetchRequestLoading || isCheckSumFetchRequestValidating) + && !isEmpty(checkSum) + && ( + <> + + { t("remoteUserStores:pages.edit.guide.steps.download.remote.verification.heading") } + + + { t("remoteUserStores:pages.edit.guide.steps" + + ".download.remote.verification.description") } + + + + + + Execute the following command in the command line. Replace the + FILE_PATH with the path of the downloaded agent zip file. + + + + + + + { t("remoteUserStores:pages.edit.guide.steps" + + ".download.remote.verification.step2") } + + + + + + ) + } +
); }; diff --git a/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.scss b/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.scss index 80f824a0349..7c15d9eec5e 100644 --- a/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.scss +++ b/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.scss @@ -23,4 +23,15 @@ margin-top: 4px; } } + + .download-agent-step { + .verification-steps-list { + list-style: decimal; + margin-left: 20px; + + li { + display: list-item; + } + } + } } diff --git a/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.tsx b/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.tsx index 3ddfbb02ea4..8dba1fc6c5f 100644 --- a/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.tsx +++ b/features/admin.remote-userstores.v1/components/edit/userstore-setup-guide.tsx @@ -34,7 +34,7 @@ import ConfigureStep from "./setup-guide/configure-step"; import GenerateTokenStep from "./setup-guide/generate-token-step"; import OnPremDownloadAgentStep from "./setup-guide/on-prem/download-step"; import OnPremRunAgentStep from "./setup-guide/on-prem/run-step"; -import RemoteDownloadAgentStep from "./setup-guide/remote/download-step"; +import RemoteDownloadAgentStep, { AgentDownloadInfoInterface } from "./setup-guide/remote/download-step"; import RemoteRunAgentStep from "./setup-guide/remote/run-step"; import "./userstore-setup-guide.scss"; @@ -80,10 +80,10 @@ export const SetupGuideTab: FunctionComponent = ( const agentDownloadURLs: { onPrem: string; remote: { - linux: string; - linuxArm: string; - mac: string; - windows: string; + linux: AgentDownloadInfoInterface; + linuxArm: AgentDownloadInfoInterface; + mac: AgentDownloadInfoInterface; + windows: AgentDownloadInfoInterface; }; } = useSelector((state: AppState) => state.config?.deployment?.extensions?.userStoreAgentUrls); @@ -142,7 +142,7 @@ export const SetupGuideTab: FunctionComponent = ( } return ( - + { t("remoteUserStores:pages.edit.guide.heading") } diff --git a/modules/i18n/src/models/namespaces/remote-user-stores-ns.ts b/modules/i18n/src/models/namespaces/remote-user-stores-ns.ts index 0672edf5a91..aa732633072 100644 --- a/modules/i18n/src/models/namespaces/remote-user-stores-ns.ts +++ b/modules/i18n/src/models/namespaces/remote-user-stores-ns.ts @@ -66,6 +66,12 @@ export interface RemoteUserStoresNS { }; remote: { description: string; + verification: { + description: string; + heading: string; + step1: string; + step2: string; + } }; }; configure: { @@ -227,5 +233,6 @@ export interface RemoteUserStoresNS { tokenGenerateError: NotificationItem; connectionStatusCheckError: NotificationItem; disconnectError: NotificationItem; + checkSumError: NotificationItem; }; } diff --git a/modules/i18n/src/translations/en-US/portals/remote-user-stores.ts b/modules/i18n/src/translations/en-US/portals/remote-user-stores.ts index 3fd6bd9a5ba..2f1ff64a743 100644 --- a/modules/i18n/src/translations/en-US/portals/remote-user-stores.ts +++ b/modules/i18n/src/translations/en-US/portals/remote-user-stores.ts @@ -96,6 +96,10 @@ export const remoteUserStores: RemoteUserStoresNS = { } }, notifications: { + checkSumError: { + description: "There was an error while retrieving the validation information.", + message: "Something went wrong!" + }, connectionStatusCheckError: { description: "There was an error while checking the connection status.", message: "Something went wrong!" @@ -207,7 +211,13 @@ export const remoteUserStores: RemoteUserStoresNS = { description: "Download and unzip the user store agent." }, remote: { - description: "Select your platform and download the corresponding user store agent:" + description: "Select your platform and download the corresponding user store agent:", + verification: { + description: "Follow the steps below to verify the downloaded file using SHA256 checksum.", + heading: "Verify the downloaded file", + step1: "Execute the following command in the command line. Replace the <1>FILE_PATH with the path of the downloaded agent zip file.", + step2: "Compare the generated checksum with the one provided below." + } } }, run: {