From 02f74296e09792e33b3787785d588071573ec0af Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Wed, 3 Apr 2024 10:42:27 +0800 Subject: [PATCH] fix(mrf): handling for incorrect submission secret key in url query params (#7219) --- .../public-form/PublicFormProvider.tsx | 94 ++++++++++++++++--- .../FormFields/FormFieldsContainer.tsx | 59 +----------- .../FormFields/SecretKeyVerification.tsx | 25 ----- .../components/FormNotFound/FormNotFound.tsx | 8 +- 4 files changed, 86 insertions(+), 100 deletions(-) delete mode 100644 frontend/src/features/public-form/components/FormFields/SecretKeyVerification.tsx diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx index d9952076e8..53b267faf7 100644 --- a/frontend/src/features/public-form/PublicFormProvider.tsx +++ b/frontend/src/features/public-form/PublicFormProvider.tsx @@ -8,7 +8,7 @@ import { } from 'react' import { Helmet } from 'react-helmet-async' import { SubmitHandler } from 'react-hook-form' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import { useDisclosure } from '@chakra-ui/react' import { datadogLogs } from '@datadog/browser-logs' import { useGrowthBook } from '@growthbook/growthbook-react' @@ -36,6 +36,7 @@ import { MONGODB_ID_REGEX } from '~constants/routes' import { useBrowserStm } from '~hooks/payments' import { useTimeout } from '~hooks/useTimeout' import { useToast } from '~hooks/useToast' +import { isKeypairValid } from '~utils/secretKeyValidation' import { HttpError } from '~services/ApiService' import { FormFieldValues } from '~templates/Field' @@ -138,6 +139,7 @@ export const PublicFormProvider = ({ // Stop querying once submissionData is present. /* enabled= */ !submissionData, ) + const { data: encryptedPreviousSubmission, isLoading: isSubmissionLoading, @@ -149,8 +151,45 @@ export const PublicFormProvider = ({ /* enabled= */ !submissionData, ) + const isLoading = isFormLoading || isSubmissionLoading + const error = publicFormError || encryptedSubmissionError + const [previousSubmission, setPreviousSubmission] = useState>() + const [isSubmissionSecretKeyInvalid, setIsSubmissionSecretKeyInvalid] = + useState(false) + + const [searchParams] = useSearchParams() + + if ( + previousSubmissionId && + encryptedPreviousSubmission && + !previousSubmission && + !isSubmissionSecretKeyInvalid + ) { + let submissionSecretKey = '' + try { + submissionSecretKey = decodeURIComponent(searchParams.get('key') ?? '') + } catch (e) { + console.log(e) + } + + const isValid = isKeypairValid( + encryptedPreviousSubmission.submissionPublicKey, + submissionSecretKey, + ) + + if (isValid) { + setPreviousSubmission( + decryptSubmission({ + submission: encryptedPreviousSubmission, + secretKey: submissionSecretKey, + }), + ) + } else { + setIsSubmissionSecretKeyInvalid(true) + } + } // Replace form fields, logic, and workflow with the previous version for MRF consistency. if (data && encryptedPreviousSubmission) { @@ -161,9 +200,6 @@ export const PublicFormProvider = ({ } } - const isLoading = isFormLoading || isSubmissionLoading - const error = publicFormError || encryptedSubmissionError - const growthbook = useGrowthBook() useEffect(() => { @@ -265,15 +301,35 @@ export const PublicFormProvider = ({ if (data) trackVisitPublicForm(data.form) }, [data]) - const isFormNotFound = useMemo(() => { - return ( - (error instanceof HttpError && - (error.code === 404 || error.code === 410)) || - (!!previousSubmissionId && - !!data && - data.form.responseMode !== FormResponseMode.Multirespondent) - ) - }, [data, error, previousSubmissionId]) + const formNotFoundMessage = useMemo(() => { + // Server response 404 or 410 + const isFormNotFound = + error instanceof HttpError && (error.code === 404 || error.code === 410) + + // Non MRFs should not use the :formId/edit/:submissionId path + const isNonMultirespondentFormWithPreviousSubmissionId = + !!data && + data.form.responseMode !== FormResponseMode.Multirespondent && + !!previousSubmissionId + + if (isFormNotFound || isNonMultirespondentFormWithPreviousSubmissionId) { + return { + title: 'Form not found', + header: 'This form is not available.', + message: error?.message || 'Form not found', + } + } + + // Decryption failed for previous submission + if (isSubmissionSecretKeyInvalid) { + return { + title: 'Invalid form link', + header: 'This form link is no longer valid.', + message: + 'A submission may have already been made using this link. If you think this is a mistake, please contact the agency that gave you the form link.', + } + } + }, [error, data, previousSubmissionId, isSubmissionSecretKeyInvalid]) const generateVfnExpiryToast = useCallback(() => { if (vfnToastIdRef.current) { @@ -713,8 +769,16 @@ export const PublicFormProvider = ({ ...rest, }} > - - {isFormNotFound ? : children} + + {formNotFoundMessage ? ( + + ) : ( + children + )} ) } diff --git a/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx b/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx index 9fc11148a7..fa2834962e 100644 --- a/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx +++ b/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx @@ -1,37 +1,27 @@ import { useMemo } from 'react' -import { useSearchParams } from 'react-router-dom' import { Box } from '@chakra-ui/react' import { FormAuthType, FormResponseMode } from '~shared/types' -import { isKeypairValid } from '~utils/secretKeyValidation' - import { usePublicFormContext } from '~features/public-form/PublicFormContext' -import { decryptSubmission } from '~features/public-form/utils/decryptSubmission' import { FormAuth } from '../FormAuth' import { FormFields } from './FormFields' import { FormFieldsSkeleton } from './FormFieldsSkeleton' -import { SecretKeyVerification } from './SecretKeyVerification' export const FormFieldsContainer = (): JSX.Element | null => { const { form, - previousSubmissionId, isAuthRequired, isLoading, handleSubmitForm, submissionData, encryptedPreviousSubmission, previousSubmission, - setPreviousSubmission, } = usePublicFormContext() - const { submissionPublicKey = null, workflowStep } = - encryptedPreviousSubmission ?? {} - const [searchParams] = useSearchParams() - const queryParams = Object.fromEntries([...searchParams]) + const { workflowStep } = encryptedPreviousSubmission ?? {} const renderFields = useMemo(() => { // Render skeleton when no data @@ -49,48 +39,6 @@ export const FormFieldsContainer = (): JSX.Element | null => { return } - // MRF - if (previousSubmissionId && !previousSubmission) { - let submissionSecretKey = '' - try { - submissionSecretKey = queryParams.key - ? decodeURIComponent(queryParams.key || '') - : '' - } catch (e) { - console.log(e) - } - - const isValid = isKeypairValid( - submissionPublicKey || '', - submissionSecretKey, - ) - - if (isValid) { - setPreviousSubmission( - decryptSubmission({ - submission: encryptedPreviousSubmission, - secretKey: submissionSecretKey, - }), - ) - } - - return ( - - setPreviousSubmission( - decryptSubmission({ - submission: encryptedPreviousSubmission, - secretKey, - }), - ) - } - isLoading={isLoading} - prefillSecretKey={submissionSecretKey} - /> - ) - } - return ( { isLoading, form, isAuthRequired, - previousSubmissionId, previousSubmission, workflowStep, handleSubmitForm, - submissionPublicKey, - queryParams.key, - setPreviousSubmission, - encryptedPreviousSubmission, ]) if (submissionData) return null diff --git a/frontend/src/features/public-form/components/FormFields/SecretKeyVerification.tsx b/frontend/src/features/public-form/components/FormFields/SecretKeyVerification.tsx deleted file mode 100644 index 0ab55709e6..0000000000 --- a/frontend/src/features/public-form/components/FormFields/SecretKeyVerification.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box } from '@chakra-ui/react' - -import SecretKeyVerificationInput from '~components/SecretKeyVerificationInput' -import { SecretKeyVerificationInputProps } from '~components/SecretKeyVerificationInput/SecretKeyVerificationInput' - -export type SecretKeyVerificationProps = Omit< - SecretKeyVerificationInputProps, - 'description' | 'isButtonFullWidth' | 'showGuideLink' | 'buttonText' -> - -export const SecretKeyVerification = ( - props: SecretKeyVerificationProps, -): JSX.Element => { - return ( - - - - ) -} diff --git a/frontend/src/features/public-form/components/FormNotFound/FormNotFound.tsx b/frontend/src/features/public-form/components/FormNotFound/FormNotFound.tsx index 4ae3d2c2ed..3b0f7aca81 100644 --- a/frontend/src/features/public-form/components/FormNotFound/FormNotFound.tsx +++ b/frontend/src/features/public-form/components/FormNotFound/FormNotFound.tsx @@ -5,10 +5,14 @@ import { FormFooter } from '../FormFooter' import { FormNotFoundSvgr } from './FormNotFoundSvgr' interface FormNotFoundProps { + header?: string message?: string } -export const FormNotFound = ({ message }: FormNotFoundProps): JSX.Element => { +export const FormNotFound = ({ + header = 'This form is not available.', + message, +}: FormNotFoundProps): JSX.Element => { return ( { textAlign="center" > - This form is not available. + {header} {message}