diff --git a/src/__tests__/RouteSelector.spec.jsx b/src/__tests__/RouteSelector.spec.jsx index 16fa6a0e1..0cd9a7727 100644 --- a/src/__tests__/RouteSelector.spec.jsx +++ b/src/__tests__/RouteSelector.spec.jsx @@ -551,7 +551,7 @@ describe("Tests for RouteSelector", () => { render( @@ -617,7 +617,7 @@ describe("Tests for RouteSelector", () => { render( @@ -693,7 +693,7 @@ describe("Tests for RouteSelector", () => { render( diff --git a/src/layouts/Media/Media.tsx b/src/layouts/Media/Media.tsx index 0cac8ceed..16db4e25d 100644 --- a/src/layouts/Media/Media.tsx +++ b/src/layouts/Media/Media.tsx @@ -58,7 +58,12 @@ import { } from "features/FeatureTour/FeatureTourSequence" import { MediaData } from "types/directory" import { MediaFolderTypes, MediaLabels, SelectedMediaDto } from "types/media" -import { DEFAULT_RETRY_MSG, useErrorToast, useSuccessToast } from "utils" +import { + DEFAULT_RETRY_MSG, + specialCharactersRegexTest, + useErrorToast, + useSuccessToast, +} from "utils" import { CreateButton } from "../components" import { SiteEditLayout } from "../layouts" @@ -259,7 +264,7 @@ export const Media = (): JSX.Element => { setSelectedMedia([]) onCreateMediaFolderModalClose() }, - onSuccess: (data, variables, context) => { + onSuccess: (data, variables) => { if (variables.selectedPages.length === 0) { successToast({ id: "create-directory-success", @@ -280,7 +285,7 @@ export const Media = (): JSX.Element => { `${url}%2F${encodeURIComponent(variables.newDirectoryName)}` ) }, - onError: (err, variables, context) => { + onError: () => { errorToast({ id: "create-directory-error", description: `Your ${singularDirectoryLabel} could not be created successfully. ${DEFAULT_RETRY_MSG}`, @@ -296,7 +301,7 @@ export const Media = (): JSX.Element => { setSelectedMedia([]) onMoveModalClose() }, - onSuccess: (data, variables, context) => { + onSuccess: (data, variables) => { successToast({ id: "move-multiple-media-success", description: `Successfully moved ${ @@ -304,7 +309,7 @@ export const Media = (): JSX.Element => { }!`, }) }, - onError: (err, variables, context) => { + onError: (err, variables) => { errorToast({ id: "move-multiple-media-error", description: `Your ${ @@ -318,7 +323,7 @@ export const Media = (): JSX.Element => { mutate: deleteMultipleMedia, isLoading: isDeleteMultipleMediaLoading, } = useDeleteMultipleMediaHook(params, { - onSettled: (data, error, variables, context) => { + onSettled: (data, error, variables) => { if (variables.length === 1) { setSelectedMedia( selectedMedia.filter((selectedData) => @@ -333,7 +338,7 @@ export const Media = (): JSX.Element => { if (individualMedia) setIndividualMedia(null) onDeleteModalClose() }, - onSuccess: (data, variables, context) => { + onSuccess: (data, variables) => { successToast({ id: "delete-multiple-media-success", description: `Successfully deleted ${ @@ -341,7 +346,7 @@ export const Media = (): JSX.Element => { }!`, }) }, - onError: (err, variables, context) => { + onError: (err, variables) => { errorToast({ id: "delete-multiple-media-error", description: `Your ${ @@ -724,16 +729,42 @@ export const Media = (): JSX.Element => { path={[`${path}/editMediaSettings/:fileName`]} component={MediaSettingsScreen} onClose={() => history.goBack()} + validate={{ + fileName: (value) => { + const encodedName = value.split(".").slice(0, -1).join(".") + return !specialCharactersRegexTest.test(encodedName) + }, + }} /> history.goBack()} + validate={{ + mediaDirectoryName: (value) => { + // NOTE: This value is prepended with either `files|images/` + // and nested directories are separated by `/` as well. + const decodedValues = decodeURIComponent(value).split("/") + return decodedValues.every( + (val) => !specialCharactersRegexTest.test(val) + ) + }, + }} /> history.goBack()} + validate={{ + mediaDirectoryName: (value) => { + // NOTE: This value is prepended with either `files|images/` + // and nested directories are separated by `/` as well. + const decodedValues = decodeURIComponent(value).split("/") + return decodedValues.every( + (val) => !specialCharactersRegexTest.test(val) + ) + }, + }} /> diff --git a/src/layouts/ResourceRoom/ResourceRoom.tsx b/src/layouts/ResourceRoom/ResourceRoom.tsx index c1d0b2281..502fea5fe 100644 --- a/src/layouts/ResourceRoom/ResourceRoom.tsx +++ b/src/layouts/ResourceRoom/ResourceRoom.tsx @@ -62,13 +62,20 @@ import { useErrorToast, useSuccessToast } from "utils/toasts" import { DirectoryData, DirectoryInfoProps } from "types/directory" import { ResourceRoomRouteParams } from "types/resources" -import { DEFAULT_RETRY_MSG, deslugifyDirectory } from "utils" +import { + DEFAULT_RETRY_MSG, + deslugifyDirectory, + resourceCategoryRegexTest, +} from "utils" import { ResourceRoomNameUpdateProps } from "../../types/directory" import { SiteEditLayout } from "../layouts" import { CategoryCard, ResourceBreadcrumb } from "./components" +const INVALID_CHAR_MESSAGE = + "Please ensure that you only use alphanumeric characters with dashes and space!" + const EmptyResourceRoom = () => { const params = useParams() const { siteName } = params @@ -142,6 +149,10 @@ const EmptyResourceRoom = () => { placeholder="Resource room name" {...register("newDirectoryName", { required: "Please enter resource room name", + pattern: { + value: resourceCategoryRegexTest, + message: INVALID_CHAR_MESSAGE, + }, })} /> @@ -375,10 +386,14 @@ const ResourceRoomContent = ({ > Resource room title @@ -394,7 +409,7 @@ const ResourceRoomContent = ({ diff --git a/src/routing/ProtectedRoute.tsx b/src/routing/ProtectedRoute.tsx index 41a282739..e271e7784 100644 --- a/src/routing/ProtectedRoute.tsx +++ b/src/routing/ProtectedRoute.tsx @@ -1,7 +1,7 @@ import { Box, Center, Spinner } from "@chakra-ui/react" import { useGrowthBook } from "@growthbook/growthbook-react" import axios from "axios" -import _ from "lodash" +import _, { identity } from "lodash" import { useEffect } from "react" import { Redirect, Route, RouteProps, useLocation } from "react-router-dom" @@ -12,6 +12,8 @@ import { getSiteNameAttributeFromPath } from "utils/growthbook" import { GBAttributes } from "types/featureFlags" +import { WithValidator } from "./types" + // axios settings axios.defaults.withCredentials = true @@ -25,7 +27,7 @@ export const ProtectedRoute = ({ children, component: WrappedComponent, ...rest -}: RouteProps): JSX.Element => { +}: WithValidator): JSX.Element => { const { displayedName, isLoading, @@ -35,8 +37,27 @@ export const ProtectedRoute = ({ contactNumber, } = useLoginContext() const growthbook = useGrowthBook() + const currPath = useLocation().pathname const siteNameFromPath = getSiteNameAttributeFromPath(currPath) + const { validate } = rest + + const validateParams = ( + params: Record | undefined + ) => { + return _.entries(params) + .map(([key, value]) => { + // NOTE: There's no provided validation function + // so we will assume it's valid + if (!validate?.[key]) return true + + // NOTE: If the value is falsy, we will return true as there's nothing to validate + if (!value) return true + + return validate[key](value) + }) + .every(identity) + } useEffect(() => { if (growthbook) { @@ -75,7 +96,19 @@ export const ProtectedRoute = ({ } if (displayedName && children) { - return {children} + return ( + + {({ match }) => { + const isValid = validateParams(match?.params) + + if (!isValid) { + return + } + + return <>{children} + }} + + ) } if (displayedName && WrappedComponent) { @@ -84,11 +117,17 @@ export const ProtectedRoute = ({ {...rest} render={(props) => { const { match } = props - const { params } = match + const isValid = validateParams(match?.params) + + if (!isValid) { + return + } + const newMatch = { ...match, - decodedParams: getDecodedParams(prune(params)), + decodedParams: getDecodedParams(prune(match.params)), } + return }} /> diff --git a/src/routing/ProtectedRouteWithProps.tsx b/src/routing/ProtectedRouteWithProps.tsx index 9b6710124..b39568bf8 100644 --- a/src/routing/ProtectedRouteWithProps.tsx +++ b/src/routing/ProtectedRouteWithProps.tsx @@ -4,13 +4,16 @@ import { RouteProps as BaseRouteProps } from "react-router-dom" import FallbackComponent from "components/FallbackComponent" import { ProtectedRoute } from "./ProtectedRoute" +import { WithValidator } from "./types" type RouteProps = { component?: () => JSX.Element onClose: () => void } & BaseRouteProps -export const ProtectedRouteWithProps = (props: RouteProps): JSX.Element => { +export const ProtectedRouteWithProps = ( + props: WithValidator +): JSX.Element => { return ( diff --git a/src/routing/RouteSelector.jsx b/src/routing/RouteSelector.jsx index e24d8658d..a40e1b931 100644 --- a/src/routing/RouteSelector.jsx +++ b/src/routing/RouteSelector.jsx @@ -35,6 +35,8 @@ import { Workspace } from "layouts/Workspace" import { ProtectedRouteWithProps } from "routing/ProtectedRouteWithProps" import RedirectIfLoggedInRoute from "routing/RedirectIfLoggedInRoute" +import { specialCharactersRegexTest } from "utils" + import { ApprovedReviewRedirect, injectApprovalRedirect, @@ -66,19 +68,48 @@ export const RouteSelector = () => { { + const encodedName = value.split(".").slice(0, -1).join(".") + return !specialCharactersRegexTest.test(encodedName) + }, + }} + /> + + { + const encodedName = value.split(".").slice(0, -1).join(".") + return !specialCharactersRegexTest.test(encodedName) + }, + subCollectionName: (value) => { + return !specialCharactersRegexTest.test(value) + }, + }} /> { + return !specialCharactersRegexTest.test(value) + }, + }} > @@ -93,8 +124,18 @@ export const RouteSelector = () => { { + // NOTE: This value is prepended with either `files|images` + // and nested directories are separated by `/` as well. + const decodedValues = decodeURIComponent(value).split("/") + return decodedValues.every( + (val) => !specialCharactersRegexTest.test(val) + ) + }, + }} > @@ -119,7 +160,7 @@ export const RouteSelector = () => { - + @@ -141,7 +182,7 @@ export const RouteSelector = () => { component={injectApprovalRedirect(EditContactUs)} /> - + @@ -149,7 +190,7 @@ export const RouteSelector = () => { diff --git a/src/routing/types.ts b/src/routing/types.ts new file mode 100644 index 000000000..7e07fff2e --- /dev/null +++ b/src/routing/types.ts @@ -0,0 +1,5 @@ +// NOTE: If it returns true, it is a valid value. +// Otherwise, it is invalid and we should reject. +// This is inline w/ validation libraries. +export type Validator = (value: string) => boolean +export type WithValidator = T & { validate?: Record }