Skip to content

Commit

Permalink
fix(mrf): handling for incorrect submission secret key in url query p…
Browse files Browse the repository at this point in the history
…arams
  • Loading branch information
justynoh committed Apr 1, 2024
1 parent 8f02d4a commit 157d9a1
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 100 deletions.
94 changes: 79 additions & 15 deletions frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -138,6 +139,7 @@ export const PublicFormProvider = ({
// Stop querying once submissionData is present.
/* enabled= */ !submissionData,
)

const {
data: encryptedPreviousSubmission,
isLoading: isSubmissionLoading,
Expand All @@ -149,8 +151,45 @@ export const PublicFormProvider = ({
/* enabled= */ !submissionData,
)

const isLoading = isFormLoading || isSubmissionLoading
const error = publicFormError || encryptedSubmissionError

const [previousSubmission, setPreviousSubmission] =
useState<ReturnType<typeof decryptSubmission>>()
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) {
Expand All @@ -161,9 +200,6 @@ export const PublicFormProvider = ({
}
}

const isLoading = isFormLoading || isSubmissionLoading
const error = publicFormError || encryptedSubmissionError

const growthbook = useGrowthBook()

useEffect(() => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -709,8 +765,16 @@ export const PublicFormProvider = ({
...rest,
}}
>
<Helmet title={isFormNotFound ? 'Form not found' : data?.form.title} />
{isFormNotFound ? <FormNotFound message={error?.message} /> : children}
<Helmet
title={
formNotFoundMessage ? formNotFoundMessage.title : data?.form.title
}
/>
{formNotFoundMessage ? (
<FormNotFound {...formNotFoundMessage} />
) : (
children
)}
</PublicFormContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -49,48 +39,6 @@ export const FormFieldsContainer = (): JSX.Element | null => {
return <FormAuth authType={form.authType} />
}

// 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 (
<SecretKeyVerification
publicKey={submissionPublicKey}
setSecretKey={(secretKey) =>
setPreviousSubmission(
decryptSubmission({
submission: encryptedPreviousSubmission,
secretKey,
}),
)
}
isLoading={isLoading}
prefillSecretKey={submissionSecretKey}
/>
)
}

return (
<FormFields
previousResponses={previousSubmission?.responses}
Expand All @@ -113,14 +61,9 @@ export const FormFieldsContainer = (): JSX.Element | null => {
isLoading,
form,
isAuthRequired,
previousSubmissionId,
previousSubmission,
workflowStep,
handleSubmitForm,
submissionPublicKey,
queryParams.key,
setPreviousSubmission,
encryptedPreviousSubmission,
])

if (submissionData) return null
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Flex flex={1} flexDir="column" h="100%">
<Flex
Expand All @@ -35,7 +39,7 @@ export const FormNotFound = ({ message }: FormNotFoundProps): JSX.Element => {
textAlign="center"
>
<Text as="h2" textStyle="h2">
This form is not available.
{header}
</Text>
<Text textStyle="body-1">{message}</Text>
</Stack>
Expand Down

0 comments on commit 157d9a1

Please sign in to comment.