Skip to content

Commit

Permalink
feat(mrf): backend validation for field locking
Browse files Browse the repository at this point in the history
  • Loading branch information
justynoh committed Mar 18, 2024
1 parent f270eea commit d3cace3
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 85 deletions.
6 changes: 6 additions & 0 deletions frontend/src/features/public-form/PublicFormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UseQueryResult } from 'react-query'
import { MultirespondentSubmissionDto } from '~shared/types'
import { PublicFormViewDto } from '~shared/types/form'

import { decryptSubmission } from './utils/decryptSubmission'

export type SubmissionData = {
/** Submission id */
id: string | undefined
Expand Down Expand Up @@ -65,6 +67,10 @@ export interface PublicFormContextProps
setNumVisibleFields?: Dispatch<SetStateAction<number>>

encryptedPreviousSubmission?: MultirespondentSubmissionDto
previousSubmission?: ReturnType<typeof decryptSubmission>
setPreviousSubmission: (
previousSubmission: ReturnType<typeof decryptSubmission>,
) => void
}

export const PublicFormContext = createContext<
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
} from '~features/verifiable-fields'

import { FormNotFound } from './components/FormNotFound'
import { decryptSubmission } from './utils/decryptSubmission'
import { usePublicAuthMutations, usePublicFormMutations } from './mutations'
import { PublicFormContext, SubmissionData } from './PublicFormContext'
import { useEncryptedSubmission, usePublicFormView } from './queries'
Expand Down Expand Up @@ -148,6 +149,9 @@ export const PublicFormProvider = ({
/* enabled= */ !submissionData,
)

const [previousSubmission, setPreviousSubmission] =
useState<ReturnType<typeof decryptSubmission>>()

// Replace form fields, logic, and workflow with the previous version for MRF consistency.
if (data && encryptedPreviousSubmission) {
data.form.form_fields = encryptedPreviousSubmission.form_fields
Expand Down Expand Up @@ -300,7 +304,11 @@ export const PublicFormProvider = ({
submitStorageModeFormFetchMutation,
submitMultirespondentFormMutation,
updateMultirespondentSubmissionMutation,
} = usePublicFormMutations(formId, previousSubmissionId)
} = usePublicFormMutations(
formId,
previousSubmissionId,
previousSubmission?.submissionSecretKey,
)

const { handleLogoutMutation } = usePublicAuthMutations(formId)

Expand Down Expand Up @@ -694,6 +702,8 @@ export const PublicFormProvider = ({
isPreview: false,
setNumVisibleFields,
encryptedPreviousSubmission,
previousSubmission,
setPreviousSubmission,
...commonFormValues,
...data,
...rest,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/features/public-form/PublicFormService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export type SubmitStorageFormWithVirusScanningArgs =

export type SubmitMultirespondentFormWithVirusScanningArgs =
SubmitEmailFormArgs & {
// publicKey: string
submissionSecretKey?: string
fieldIdToQuarantineKeyMap: FieldIdToQuarantineKeyType[]
}

Expand Down Expand Up @@ -369,6 +369,7 @@ export const updateMultirespondentSubmission = async ({
captchaType = '',
responseMetadata,
fieldIdToQuarantineKeyMap,
submissionSecretKey,
}: SubmitMultirespondentFormWithVirusScanningArgs & {
submissionId?: string
}) => {
Expand All @@ -383,6 +384,7 @@ export const updateMultirespondentSubmission = async ({
formFields,
formInputs: filteredInputs,
responseMetadata,
submissionSecretKey,
version: MULTIRESPONDENT_FORM_SUBMISSION_VERSION,
},
fieldIdToQuarantineKeyMap,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Box } from '@chakra-ui/react'

Expand All @@ -24,11 +24,10 @@ export const FormFieldsContainer = (): JSX.Element | null => {
handleSubmitForm,
submissionData,
encryptedPreviousSubmission,
previousSubmission,
setPreviousSubmission,
} = usePublicFormContext()

const [previousSubmission, setPreviousSubmission] =
useState<ReturnType<typeof decryptSubmission>>()

const { submissionPublicKey = null, workflowStep } =
encryptedPreviousSubmission ?? {}
const [searchParams] = useSearchParams()
Expand Down Expand Up @@ -120,6 +119,7 @@ export const FormFieldsContainer = (): JSX.Element | null => {
handleSubmitForm,
submissionPublicKey,
queryParams.key,
setPreviousSubmission,
encryptedPreviousSubmission,
])

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/features/public-form/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const usePublicAuthMutations = (formId: string) => {
export const usePublicFormMutations = (
formId: string,
submissionId?: string,
submissionSecretKey?: string,
) => {
const submitEmailModeFormMutation = useMutation(
(args: Omit<SubmitEmailFormArgs, 'formId'>) => {
Expand Down Expand Up @@ -176,7 +177,9 @@ export const usePublicFormMutations = (
)

const updateMultirespondentSubmissionMutation =
useSubmitStorageModeFormMutation(updateMultirespondentSubmission)
useSubmitStorageModeFormMutation((args) =>
updateMultirespondentSubmission({ ...args, submissionSecretKey }),
)

return {
submitEmailModeFormMutation,
Expand Down
51 changes: 47 additions & 4 deletions frontend/src/features/public-form/utils/createSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type CreateStorageSubmissionFormDataArgs = CreateEmailSubmissionFormDataArgs & {

type CreateMultirespondentSubmissionFormDataArgs =
CreateEmailSubmissionFormDataArgs & {
submissionSecretKey?: string
version: number
}

Expand Down Expand Up @@ -268,14 +269,55 @@ const createResponsesV3 = (
case BasicField.Uen:
case BasicField.Date:
case BasicField.CountryRegion:
case BasicField.YesNo:
case BasicField.YesNo: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input) continue
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input,
} as FieldResponseV3
break
}
case BasicField.Email:
case BasicField.Mobile:
case BasicField.Table:
case BasicField.Checkbox:
case BasicField.Mobile: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input || !input.value) continue
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input,
} as FieldResponseV3
break
}
case BasicField.Table: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input) continue
if (input.every((row) => Object.values(row).every((value) => !value))) {
continue
}
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input,
} as FieldResponseV3
break
}
case BasicField.Checkbox: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input) continue
if ((!input.value || input.value.length === 0) && !input.othersInput) {
continue
}
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input,
} as FieldResponseV3
break
}
case BasicField.Children: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input) continue
if (input.child.every((child) => child.every((value) => !value))) {
continue
}
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input,
Expand Down Expand Up @@ -305,6 +347,7 @@ const createResponsesV3 = (
case BasicField.Radio: {
const input = formInputs[ff._id] as FormFieldValue<typeof ff.fieldType>
if (!input) continue
if (!input.value && !input.othersInput) continue
returnedInputs[ff._id] = {
fieldType: ff.fieldType,
answer: input.othersInput
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/features/public-form/utils/decryptSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const decryptSubmission = ({
}):
| (Omit<MultirespondentSubmissionDto, 'encryptedContent' | 'version'> & {
responses: FieldResponsesV3
submissionSecretKey: string
})
| undefined => {
if (!submission) throw Error('Encrypted submission undefined')
Expand All @@ -28,5 +29,6 @@ export const decryptSubmission = ({
return {
...rest,
responses: decryptedContent.responses as FieldResponsesV3,
submissionSecretKey: secretKey,
}
}
106 changes: 106 additions & 0 deletions shared/utils/response-v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { BasicField, FieldResponseV3 } from '../types'

const areArraysEqual = <T>(
array1: T[],
array2: T[],
eq: (value1: T, value2: T) => boolean,
): boolean =>
array1.length === array2.length &&
array1.every((value1, i) => eq(value1, array2[i]))

export const areFieldResponseV3sEqual = (
response1: FieldResponseV3,
response2: FieldResponseV3,
): boolean => {
if (response1.fieldType !== response2.fieldType) return false

switch (response1.fieldType) {
case BasicField.Number:
case BasicField.Decimal:
case BasicField.ShortText:
case BasicField.LongText:
case BasicField.HomeNo:
case BasicField.Dropdown:
case BasicField.Rating:
case BasicField.Nric:
case BasicField.Uen:
case BasicField.Date:
case BasicField.CountryRegion:
case BasicField.YesNo:
return response1.answer === response2.answer

case BasicField.Attachment: {
const response2Answer = response2.answer as typeof response1.answer
return (
response1.answer.answer === response2Answer.answer &&
response1.answer.hasBeenScanned === response2Answer.hasBeenScanned
)
}
case BasicField.Email:
case BasicField.Mobile: {
const response2Answer = response2.answer as typeof response1.answer
return (
response1.answer.value === response2Answer.value &&
response1.answer.signature === response2Answer.signature
)
}
case BasicField.Table: {
const response2Answer = response2.answer as typeof response1.answer
return areArraysEqual(
response1.answer,
response2Answer,
(row1, row2) =>
Object.keys(row1).length === Object.keys(row2).length &&
Object.keys(row1).every(
(columnId) => row1[columnId] === row2[columnId],
),
)
}
case BasicField.Radio: {
if ('value' in response1.answer) {
const response2Answer = response2.answer as typeof response1.answer
return response1.answer.value === response2Answer.value
} else {
const response2Answer = response2.answer as typeof response1.answer
return response1.answer.othersInput === response2Answer.othersInput
}
}
case BasicField.Checkbox: {
const response2Answer = response2.answer as typeof response1.answer
return (
areArraysEqual(
response1.answer.value,
response2Answer.value,
(value1, value2) => value1 === value2,
) && response1.answer.othersInput === response2Answer.othersInput
)
}
case BasicField.Children: {
const response2Answer = response2.answer as typeof response1.answer
return (
areArraysEqual(
response1.answer.child,
response2Answer.child,
(child1, child2) =>
areArraysEqual(
child1,
child2,
(value1, value2) => value1 === value2,
),
) &&
areArraysEqual(
response1.answer.childFields,
response2Answer.childFields,
(attr1, attr2) => attr1 === attr2,
)
)
}
case BasicField.Section:
return true
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = response1
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,9 @@ const updateMultirespondentSubmission = async (
await submission.save()
} catch (err) {
logger.error({
message: 'Encrypt submission save error',
message: 'Multirespondent submission save error',
meta: {
action: 'onEncryptSubmissionFailure',
action: 'onMultirespondentSubmissionFailure',
...createReqMeta(req),
},
error: err,
Expand Down Expand Up @@ -491,8 +491,7 @@ export const handleMultirespondentSubmission = [
MultirespondentSubmissionMiddleware.validateMultirespondentSubmissionParams,
MultirespondentSubmissionMiddleware.createFormsgAndRetrieveForm,
MultirespondentSubmissionMiddleware.scanAndRetrieveAttachments,
// TODO(MRF/FRM-1592): Add validation for FieldResponsesV3
// EncryptSubmissionMiddleware.validateStorageSubmission,
MultirespondentSubmissionMiddleware.validateMultirespondentSubmission,
MultirespondentSubmissionMiddleware.encryptSubmission,
submitMultirespondentForm,
] as ControllerHandler[]
Expand All @@ -501,10 +500,10 @@ export const handleUpdateMultirespondentSubmission = [
CaptchaMiddleware.validateCaptchaParams,
TurnstileMiddleware.validateTurnstileParams,
ReceiverMiddleware.receiveMultirespondentSubmission,
MultirespondentSubmissionMiddleware.validateMultirespondentSubmissionParams,
MultirespondentSubmissionMiddleware.validateUpdateMultirespondentSubmissionParams,
MultirespondentSubmissionMiddleware.createFormsgAndRetrieveForm,
MultirespondentSubmissionMiddleware.scanAndRetrieveAttachments,
// EncryptSubmissionMiddleware.validateStorageSubmission,
MultirespondentSubmissionMiddleware.validateMultirespondentSubmission,
MultirespondentSubmissionMiddleware.setCurrentWorkflowStep,
MultirespondentSubmissionMiddleware.encryptSubmission,
updateMultirespondentSubmission,
Expand Down

0 comments on commit d3cace3

Please sign in to comment.