diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d77235946..6d6ad2fb9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -#### [v6.76.1](https://github.com/opengovsg/FormSG/compare/v6.76.0...v6.76.1) +#### [v6.77.0](https://github.com/opengovsg/FormSG/compare/v6.76.0...v6.77.0) +- build: merge v6.76.1 into develop [`#6708`](https://github.com/opengovsg/FormSG/pull/6708) +- feat: soften and fix storage submission validation [`#6696`](https://github.com/opengovsg/FormSG/pull/6696) +- build: release v6.76.1 [`#6702`](https://github.com/opengovsg/FormSG/pull/6702) - fix: invalid mixed digit input [`#6701`](https://github.com/opengovsg/FormSG/pull/6701) +- build: merge v6.76.0 into develop [`#6700`](https://github.com/opengovsg/FormSG/pull/6700) - build: release v6.76.0 [`#6698`](https://github.com/opengovsg/FormSG/pull/6698) +- chore: bump version to 6.76.1 [`9e88567`](https://github.com/opengovsg/FormSG/commit/9e8856721af4e3a99b6c6fe0e31ac96414a46149) #### [v6.76.0](https://github.com/opengovsg/FormSG/compare/v6.75.1...v6.76.0) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e5024cfe55..1acc77c2b7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.76.0", + "version": "6.77.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.76.0", + "version": "6.77.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", diff --git a/frontend/package.json b/frontend/package.json index 3d82c88056..e5f59edd00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.76.0", + "version": "6.77.0", "homepage": ".", "private": true, "dependencies": { diff --git a/package-lock.json b/package-lock.json index 2948edf654..8016a6d6fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.76.1", + "version": "6.77.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.76.1", + "version": "6.77.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.347.1", diff --git a/package.json b/package.json index 6b2ef28274..2eb45d9e27 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.76.1", + "version": "6.77.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " diff --git a/shared/constants/feature-flags.ts b/shared/constants/feature-flags.ts index 42f8171758..f73ca2c65e 100644 --- a/shared/constants/feature-flags.ts +++ b/shared/constants/feature-flags.ts @@ -4,4 +4,6 @@ export const featureFlags = { turnstile: 'turnstile' as const, validateStripeEmailDomain: 'validateStripeEmailDomain' as const, encryptionBoundaryShift: 'encryption-boundary-shift' as const, + encryptionBoundaryShiftHardValidation: + 'encryption-boundary-shift-hard-validation' as const, } diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index be1f5d41ee..b823057a56 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -5420,7 +5420,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).toHaveBeenCalledWith( MOCK_FORM.form_fields, @@ -5955,7 +5954,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( @@ -6013,7 +6011,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( @@ -6071,7 +6068,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( @@ -6129,7 +6125,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).toHaveBeenCalledWith( MOCK_FORM.form_fields, @@ -6196,7 +6191,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).toHaveBeenCalledWith( MOCK_FORM.form_fields, @@ -6263,7 +6257,6 @@ describe('admin-form.controller', () => { expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, - false, ) expect(MockAdminFormService.extractMyInfoFieldIds).toHaveBeenCalledWith( MOCK_FORM.form_fields, @@ -6398,7 +6391,6 @@ describe('admin-form.controller', () => { MOCK_FORM, MOCK_RESPONSES, MOCK_ENCRYPTED_CONTENT, - true, ) expect( MockEncryptSubmissionService.createEncryptSubmissionWithoutSave, @@ -6801,7 +6793,6 @@ describe('admin-form.controller', () => { MOCK_FORM, MOCK_RESPONSES, MOCK_ENCRYPTED_CONTENT, - true, ) expect( MockEncryptSubmissionService.createEncryptSubmissionWithoutSave, @@ -6855,7 +6846,6 @@ describe('admin-form.controller', () => { MOCK_FORM, MOCK_RESPONSES, MOCK_ENCRYPTED_CONTENT, - true, ) expect( MockEncryptSubmissionService.createEncryptSubmissionWithoutSave, @@ -6909,7 +6899,6 @@ describe('admin-form.controller', () => { MOCK_FORM, MOCK_RESPONSES, MOCK_ENCRYPTED_CONTENT, - true, ) expect( MockEncryptSubmissionService.createEncryptSubmissionWithoutSave, @@ -6963,7 +6952,6 @@ describe('admin-form.controller', () => { MOCK_FORM, MOCK_RESPONSES, MOCK_ENCRYPTED_CONTENT, - true, ) expect( MockEncryptSubmissionService.createEncryptSubmissionWithoutSave, diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 2a7e615e55..4597e6e137 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -1679,7 +1679,7 @@ export const submitEncryptPreview: ControllerHandler< }), ) .andThen((form) => - IncomingEncryptSubmission.init(form, responses, encryptedContent, true) // set as true as there's no need to gate anything if the its not a real submission + IncomingEncryptSubmission.init(form, responses, encryptedContent) .map((incomingSubmission) => ({ incomingSubmission, form })) .mapErr((error) => { logger.error({ @@ -1779,7 +1779,7 @@ export const submitEmailPreview: ControllerHandler< const parsedResponsesResult = await SubmissionService.validateAttachments( responses, form.responseMode, - ).andThen(() => ParsedResponsesObject.parseResponses(form, responses, false)) // email mode submissions (esp previews) does not need to use encryption boundary shift code + ).andThen(() => ParsedResponsesObject.parseResponses(form, responses)) if (parsedResponsesResult.isErr()) { logger.error({ message: 'Error while parsing responses for preview submission', diff --git a/src/app/modules/submission/ParsedResponsesObject.class.ts b/src/app/modules/submission/ParsedResponsesObject.class.ts index f867ee6343..0640a4190c 100644 --- a/src/app/modules/submission/ParsedResponsesObject.class.ts +++ b/src/app/modules/submission/ParsedResponsesObject.class.ts @@ -84,16 +84,11 @@ export default class ParsedResponsesObject { static parseResponses( form: IFormDocument, responses: FieldResponse[], - encryptionBoundaryShiftEnabled: boolean, ): Result< ParsedResponsesObject, ProcessingError | ConflictError | ValidateFieldError > { - const filteredResponsesResult = getFilteredResponses( - form, - responses, - encryptionBoundaryShiftEnabled, - ) + const filteredResponsesResult = getFilteredResponses(form, responses, false) if (filteredResponsesResult.isErr()) { return err(filteredResponsesResult.error) } diff --git a/src/app/modules/submission/email-submission/email-submission.controller.ts b/src/app/modules/submission/email-submission/email-submission.controller.ts index 852e2d2bb1..2cc6f73dde 100644 --- a/src/app/modules/submission/email-submission/email-submission.controller.ts +++ b/src/app/modules/submission/email-submission/email-submission.controller.ts @@ -178,11 +178,7 @@ const submitEmailModeForm: ControllerHandler< form.responseMode, ) .andThen(() => - ParsedResponsesObject.parseResponses( - form, - req.body.responses, - false, // email mode submissions do not need to use encryption boundary shift code - ), + ParsedResponsesObject.parseResponses(form, req.body.responses), ) .map((parsedResponses) => ({ parsedResponses, form })) .mapErr((error) => { diff --git a/src/app/modules/submission/encrypt-submission/IncomingEncryptSubmission.class.ts b/src/app/modules/submission/encrypt-submission/IncomingEncryptSubmission.class.ts index 755e91923f..2a64ac0c00 100644 --- a/src/app/modules/submission/encrypt-submission/IncomingEncryptSubmission.class.ts +++ b/src/app/modules/submission/encrypt-submission/IncomingEncryptSubmission.class.ts @@ -1,4 +1,4 @@ -import { ok, Result } from 'neverthrow' +import { Result } from 'neverthrow' import { FieldResponse, IPopulatedEncryptedForm } from '../../../../types' import { checkIsEncryptedEncoding } from '../../../utils/encryption' @@ -32,17 +32,12 @@ export default class IncomingEncryptSubmission extends IncomingSubmission { form: IPopulatedEncryptedForm, responses: FieldResponse[], encryptedContent: string, - encryptionBoundaryShiftEnabled: boolean, ): Result< IncomingEncryptSubmission, ProcessingError | ConflictError | ValidateFieldError[] > { return checkIsEncryptedEncoding(encryptedContent) - .andThen(() => { - if (encryptionBoundaryShiftEnabled) - return ok(responses as FilteredResponse[]) - else return getFilteredResponses(form, responses, false) - }) + .andThen(() => getFilteredResponses(form, responses, true)) .andThen((filteredResponses) => this.getFieldMap(form, filteredResponses).map((fieldMap) => ({ responses: filteredResponses, diff --git a/src/app/modules/submission/encrypt-submission/__tests__/IncomingEncryptSubmission.class.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/IncomingEncryptSubmission.class.spec.ts index 05439fffe9..235d1aba6f 100644 --- a/src/app/modules/submission/encrypt-submission/__tests__/IncomingEncryptSubmission.class.spec.ts +++ b/src/app/modules/submission/encrypt-submission/__tests__/IncomingEncryptSubmission.class.spec.ts @@ -74,7 +74,6 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, responses, '', - false, ) expect(initResult._unsafeUnwrap().responses).toEqual(responses) }) @@ -129,61 +128,11 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, responses, '', - false, ) const filteredResponses = [mobileResponse, emailResponse] expect(initResult._unsafeUnwrap().responses).toEqual(filteredResponses) }) - it('should not filter responses when new encryption boundary flag is on', () => { - mockCheckIsEncryptedEncoding.mockReturnValueOnce(ok(true)) - const mobileField = generateDefaultField(BasicField.Mobile, { - isVerifiable: true, - }) - const emailField = generateDefaultField(BasicField.Email, { - isVerifiable: true, - autoReplyOptions: { - hasAutoReply: true, - autoReplySubject: 'subject', - autoReplySender: 'sender@test.gov.sg', - autoReplyMessage: 'message', - includeFormSummary: false, - }, - }) - const yesNoField = generateDefaultField(BasicField.YesNo) - const ratingField = generateDefaultField(BasicField.Rating) - const basicFormFields = [mobileField, emailField, yesNoField, ratingField] - const mobileResponse = generateSingleAnswerResponse( - mobileField, - '+6587654321', - 'signature', - ) - const emailResponse = generateSingleAnswerResponse( - emailField, - 'test@example.com', - 'signature', - ) - const yesNoResponse = generateSingleAnswerResponse(yesNoField, 'Yes') - const ratingResponse = generateSingleAnswerResponse(ratingField, '5') - const responses = [ - mobileResponse, - emailResponse, - yesNoResponse, - ratingResponse, - ] - const filteredResponses = responses - const initResult = IncomingEncryptSubmission.init( - { - responseMode: FormResponseMode.Encrypt, - form_fields: basicFormFields, - } as unknown as IPopulatedEncryptedForm, - responses, - '', - true, - ) - expect(initResult._unsafeUnwrap().responses).toEqual(filteredResponses) - }) - it('should fail when responses are missing', () => { mockCheckIsEncryptedEncoding.mockReturnValueOnce(ok(true)) const mobileField = generateDefaultField(BasicField.Mobile, { @@ -212,7 +161,6 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, responses, '', - false, ) expect(initResult._unsafeUnwrapErr()).toEqual( new ConflictError('Some form fields are missing'), @@ -260,7 +208,6 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, responses, '', - false, ) expect(result.isOk()).toEqual(true) @@ -283,7 +230,6 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, [mobileResponse], '', - false, ) expect(result.isErr()).toEqual(true) @@ -309,7 +255,6 @@ describe('IncomingEncryptSubmission', () => { } as unknown as IPopulatedEncryptedForm, [], '', - false, ) expect(result.isErr()).toEqual(true) diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index 84c3694da4..2b3ea87e01 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -7,7 +7,6 @@ import mongoose from 'mongoose' import { okAsync } from 'neverthrow' import Stripe from 'stripe' -import { featureFlags } from '../../../../../shared/constants' import { ErrorDto, FormAuthType, @@ -23,7 +22,7 @@ import { IPopulatedEncryptedForm, StripePaymentMetadataDto, } from '../../../../types' -import { FormsgCompleteDto } from '../../../../types/api' +import { FormCompleteDto } from '../../../../types/api' import config from '../../../config/config' import { paymentConfig } from '../../../config/features/payment.config' import { createLoggerWithLabel } from '../../../config/logger' @@ -77,7 +76,6 @@ import { getPaymentIntentDescription, mapRouteError, } from './encrypt-submission.utils' -import IncomingEncryptSubmission from './IncomingEncryptSubmission.class' export const logger = createLoggerWithLabel(module) const EncryptSubmission = getEncryptSubmissionModel(mongoose) @@ -130,23 +128,8 @@ const submitEncryptModeForm = async ( const encryptedPayload = req.formsg.encryptedPayload // Create Incoming Submission - const { encryptedContent, responses, responseMetadata, paymentProducts } = + const { encryptedContent, responseMetadata, paymentProducts } = encryptedPayload - const incomingSubmissionResult = IncomingEncryptSubmission.init( - form, - responses, - encryptedContent, - req.formsg.featureFlags.includes(featureFlags.encryptionBoundaryShift), - ) - if (incomingSubmissionResult.isErr()) { - const { statusCode, errorMessage } = mapRouteError( - incomingSubmissionResult.error, - ) - return res.status(statusCode).json({ - message: errorMessage, - }) - } - const incomingSubmission = incomingSubmissionResult.value // Checks if user is SPCP-authenticated before allowing submission let uinFin @@ -294,7 +277,7 @@ const submitEncryptModeForm = async ( form: form._id, authType: form.authType, myInfoFields: form.getUniqueMyInfoAttrs(), - encryptedContent: incomingSubmission.encryptedContent, + encryptedContent: encryptedContent, verifiedContent: verified, attachmentMetadata, version: req.formsg.encryptedPayload.version, @@ -312,7 +295,7 @@ const submitEncryptModeForm = async ( form, logMeta, formId, - incomingSubmission, + responses: req.formsg.filteredResponses, paymentProducts, responseMetadata, submissionContent, @@ -324,7 +307,7 @@ const submitEncryptModeForm = async ( res, logMeta, formId, - incomingSubmission, + responses: req.formsg.filteredResponses, responseMetadata, submissionContent, }) @@ -336,12 +319,14 @@ const _createPaymentSubmission = async ({ form, logMeta, formId, - incomingSubmission, + responses, submissionContent, responseMetadata, paymentProducts, }: { - req: Parameters[0] & FormsgCompleteDto + req: Parameters[0] & { + formsg: FormCompleteDto + } res: Parameters[1] form: IPopulatedEncryptedForm paymentProducts: StorageModeSubmissionContentDto['paymentProducts'] @@ -405,7 +390,7 @@ const _createPaymentSubmission = async ({ targetAccountId, amount, email: paymentReceiptEmail, - responses: incomingSubmission.responses, + responses, ...(isPaymentTypeProducts ? { products: paymentProducts } : {}), gstEnabled: form.payments_field.gst_enabled, payment_fields_snapshot: form.payments_field, @@ -566,7 +551,7 @@ const _createSubmission = async ({ logMeta, formId, responseMetadata, - incomingSubmission, + responses, }: { req: Parameters[0] res: Parameters[1] @@ -618,10 +603,7 @@ const _createSubmission = async ({ timestamp: (submission.created || new Date()).getTime(), }) - return await performEncryptPostSubmissionActions( - submission, - incomingSubmission.responses, - ) + return await performEncryptPostSubmissionActions(submission, responses) } // TODO (FRM-1232): remove endpoint after encryption boundary is shifted @@ -630,6 +612,7 @@ export const handleEncryptedSubmission = [ TurnstileMiddleware.validateTurnstileParams, EncryptSubmissionMiddleware.validateEncryptSubmissionParams, EncryptSubmissionMiddleware.createFormsgAndRetrieveForm, + EncryptSubmissionMiddleware.validateEncryptSubmission, EncryptSubmissionMiddleware.moveEncryptedPayload, submitEncryptModeForm, ] as ControllerHandler[] @@ -641,7 +624,7 @@ export const handleStorageSubmission = [ EncryptSubmissionMiddleware.validateStorageSubmissionParams, EncryptSubmissionMiddleware.createFormsgAndRetrieveForm, EncryptSubmissionMiddleware.checkNewBoundaryEnabled, - EncryptSubmissionMiddleware.validateSubmission, + EncryptSubmissionMiddleware.validateStorageSubmission, EncryptSubmissionMiddleware.encryptSubmission, submitEncryptModeForm, ] as ControllerHandler[] diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts index 0732a944ac..97e64f824d 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts @@ -10,7 +10,11 @@ import { StorageModeAttachment, StorageModeAttachmentsMap, } from '../../../../../shared/types' -import { EncryptFormFieldResponse, FormLoadedDto } from '../../../../types/api' +import { + EncryptAttachmentResponse, + EncryptFormFieldResponse, + FormLoadedDto, +} from '../../../../types/api' import { paymentConfig } from '../../../config/features/payment.config' import formsgSdk from '../../../config/formsg-sdk' import { createLoggerWithLabel } from '../../../config/logger' @@ -21,7 +25,7 @@ import * as FormService from '../../form/form.service' import ParsedResponsesObject from '../ParsedResponsesObject.class' import { sharedSubmissionParams } from '../submission.constants' import * as SubmissionService from '../submission.service' -import { getFilteredResponses, isAttachmentResponse } from '../submission.utils' +import { isAttachmentResponse } from '../submission.utils' import { EncryptedPayloadExistsError, @@ -35,8 +39,10 @@ import { EncryptSubmissionMiddlewareHandlerType, StorageSubmissionMiddlewareHandlerRequest, StorageSubmissionMiddlewareHandlerType, + ValidateSubmissionMiddlewareHandlerRequest, } from './encrypt-submission.types' import { mapRouteError } from './encrypt-submission.utils' +import IncomingEncryptSubmission from './IncomingEncryptSubmission.class' export const logger = createLoggerWithLabel(module) @@ -159,17 +165,21 @@ export const checkNewBoundaryEnabled = async ( return next() } -export const validateSubmission = async ( - req: StorageSubmissionMiddlewareHandlerRequest, +/** + * Validates storage submissions to the new endpoint (/api/v3/forms/:formId/submissions/storage). + * This uses the same validators as email mode submissions. + */ +export const validateStorageSubmission = async ( + req: ValidateSubmissionMiddlewareHandlerRequest, res: Parameters[1], next: NextFunction, ) => { const formDef = req.formsg.formDef const logMeta = { - action: 'validateSubmission', + action: 'validateStorageSubmission', ...createReqMeta(req), - formId: formDef.id, + formId: formDef._id.toString(), } // Validate submission @@ -178,23 +188,50 @@ export const validateSubmission = async ( formDef.responseMode, ) .andThen(() => - ParsedResponsesObject.parseResponses( - formDef, - req.body.responses, - req.formsg.featureFlags.includes(featureFlags.encryptionBoundaryShift), - ), + ParsedResponsesObject.parseResponses(formDef, req.body.responses), ) - .map(() => next()) + .map((parsedResponses) => { + const responses = [] as EncryptFormFieldResponse[] + for (const response of parsedResponses.getAllResponses()) { + if (response.isVisible) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { isVisible: _, ...rest } = response + if (!isAttachmentResponse(rest)) responses.push(rest) + else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { filename: __, content: ___, ...restAttachments } = rest + responses.push({ ...restAttachments } as EncryptAttachmentResponse) + } + } + } + req.formsg.filteredResponses = responses + return next() + }) .mapErr((error) => { - logger.error({ - message: 'Error processing responses', + // TODO(FRM-1318): Set DB flag to true to harden submission validation after validation has similar error rates as email mode forms. + if ( + req.formsg.featureFlags.includes( + featureFlags.encryptionBoundaryShiftHardValidation, + ) + ) { + logger.error({ + message: 'Error processing responses', + meta: logMeta, + error, + }) + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ + message: errorMessage, + }) + } + logger.warn({ + message: + 'Error processing responses, but proceeding with submission as submission have been validated client-side', meta: logMeta, error, }) - const { statusCode, errorMessage } = mapRouteError(error) - return res.status(statusCode).json({ - message: errorMessage, - }) + req.formsg.filteredResponses = req.body.responses + return next() }) } @@ -260,18 +297,11 @@ export const encryptSubmission = async ( res: Parameters[1], next: NextFunction, ) => { - const formId = req.params.formId const encryptedFormDef = req.formsg.encryptedFormDef const publicKey = encryptedFormDef.publicKey const attachmentsMap: Record = {} - const logMeta = { - action: 'encryptSubmission', - ...createReqMeta(req), - formId, - } - // Populate attachment map req.body.responses.filter(isAttachmentResponse).forEach((response) => { const fieldId = response._id @@ -284,22 +314,6 @@ export const encryptSubmission = async ( publicKey, ) - const filteredResponses = getFilteredResponses( - encryptedFormDef, - req.body.responses, - req.formsg.featureFlags.includes(featureFlags.encryptionBoundaryShift), - ) - - if (filteredResponses.isErr()) { - logger.warn({ - message: filteredResponses.error.message, - meta: logMeta, - }) - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: filteredResponses.error.message, - }) - } - const strippedBodyResponses = req.body.responses.map((response) => { if (isAttachmentResponse(response)) { return { @@ -319,7 +333,7 @@ export const encryptSubmission = async ( req.formsg.encryptedPayload = { attachments: encryptedAttachments, - responses: filteredResponses.value as EncryptFormFieldResponse[], + responses: req.formsg.filteredResponses, encryptedContent, version: req.body.version, paymentProducts: req.body.paymentProducts, @@ -347,6 +361,57 @@ export const moveEncryptedPayload = async ( return next() } +/** + * Validates mobile and email fields for storage submissions on the old endpoint. + * Should only be used for the old storage mode submission endpoint (/api/v3/forms/:formId/submissions/encrypt). + */ +export const validateEncryptSubmission = async ( + req: EncryptSubmissionMiddlewareHandlerRequest, + res: Parameters[1], + next: NextFunction, +) => { + const form = req.formsg.encryptedFormDef + const responses = req.body.responses + const encryptedContent = req.body.encryptedContent + + const logMeta = { + action: 'validateEncryptSubmission', + ...createReqMeta(req), + formId: form._id.toString(), + } + + const incomingSubmissionResult = IncomingEncryptSubmission.init( + form, + responses, + encryptedContent, + ) + if (incomingSubmissionResult.isErr()) { + logger.error({ + message: 'Error in getting parsed responses for encrypt submission', + meta: logMeta, + error: incomingSubmissionResult.error, + }) + const { statusCode, errorMessage } = mapRouteError( + incomingSubmissionResult.error, + ) + return res.status(statusCode).json({ + message: errorMessage, + }) + } + + logger.info({ + message: 'Successfully parsed responses for encrypt submission', + meta: logMeta, + }) + + req.formsg.filteredResponses = incomingSubmissionResult.value.responses + + return next() +} + +/** + * Creates formsg namespace in req.body and populates it with featureFlags, formDef and encryptedFormDef. + */ export const createFormsgAndRetrieveForm = async ( req: CreateFormsgAndRetrieveFormMiddlewareHandlerRequest, res: Parameters[1], diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.types.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.types.ts index 2c8d78ef34..84d1f225f7 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.types.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.types.ts @@ -5,8 +5,9 @@ import { import { IPopulatedEncryptedForm } from '../../../../types' import { EncryptSubmissionDto, + FormCompleteDto, + FormFilteredResponseDto, FormLoadedDto, - FormsgCompleteDto, ParsedStorageModeSubmissionBody, } from '../../../../types/api' import { ControllerHandler } from '../../core/core.types' @@ -43,7 +44,12 @@ export type StorageSubmissionMiddlewareHandlerType = ControllerHandler< export type StorageSubmissionMiddlewareHandlerRequest = Parameters[0] & { - formsg: FormLoadedDto + formsg: FormCompleteDto + } + +export type ValidateSubmissionMiddlewareHandlerRequest = + Parameters[0] & { + formsg: FormFilteredResponseDto } export type EncryptSubmissionMiddlewareHandlerType = ControllerHandler< @@ -54,7 +60,9 @@ export type EncryptSubmissionMiddlewareHandlerType = ControllerHandler< > export type EncryptSubmissionMiddlewareHandlerRequest = - Parameters[0] & FormsgCompleteDto + Parameters[0] & { + formsg: FormCompleteDto + } export type SubmitEncryptModeFormHandlerType = ControllerHandler< { formId: string }, @@ -62,4 +70,4 @@ export type SubmitEncryptModeFormHandlerType = ControllerHandler< > export type SubmitEncryptModeFormHandlerRequest = - Parameters[0] & FormsgCompleteDto + Parameters[0] & { formsg: FormCompleteDto } diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index aa52d59069..19e406bb12 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -369,7 +369,6 @@ export const validateAttachments = ( invalidExtensions, }, }) - console.log('invalidExtensions', invalidExtensions) return errAsync(new InvalidFileExtensionError()) } return okAsync(true as const) diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 761fdb1ded..8fb9edf09e 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -149,11 +149,8 @@ export const extractEmailConfirmationData = ( export const getFilteredResponses = ( form: IFormDocument, responses: FieldResponse[], - encryptionBoundaryShiftEnabled: boolean, + isEncryptedMode: boolean, ): Result => { - const isEncryptedMode = - form.responseMode === FormResponseMode.Encrypt && - !encryptionBoundaryShiftEnabled const responseModeFilter = getResponseModeFilter(isEncryptedMode) const formFieldModeFilter = getFormFieldModeFilter(isEncryptedMode) diff --git a/src/types/api/encrypt_submission.ts b/src/types/api/encrypt_submission.ts index 521e750ec1..9f0ce3b680 100644 --- a/src/types/api/encrypt_submission.ts +++ b/src/types/api/encrypt_submission.ts @@ -1,5 +1,7 @@ import type { Merge } from 'type-fest' +import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' + import { AttachmentResponse, FieldResponse, @@ -10,10 +12,11 @@ import { import { IPopulatedEncryptedForm, IPopulatedForm } from '../form' import { ParsedEmailModeSubmissionBody } from './email_submission' +import { ParsedClearFormFieldResponse } from './submission' export type EncryptSubmissionDto = Merge< StorageModeSubmissionContentDto, - { responses: EncryptFormFieldResponse[] } + { responses: EncryptFormFieldResponse[] | ProcessedFieldResponse[] } > export type EncryptAttachmentResponse = AttachmentResponse & { @@ -42,14 +45,10 @@ export type FormLoadedDto = { encryptedFormDef: IPopulatedEncryptedForm } -export type EncryptingPayloadDto = { - formsg: FormLoadedDto & { - encryptedPayload?: EncryptSubmissionDto - } +export type FormFilteredResponseDto = FormLoadedDto & { + filteredResponses: ParsedClearFormFieldResponse[] } -export type FormsgCompleteDto = { - formsg: FormLoadedDto & { - encryptedPayload: EncryptSubmissionDto - } +export type FormCompleteDto = FormFilteredResponseDto & { + encryptedPayload: EncryptSubmissionDto }