From afc73ab42f1ff5fead6624501a83a60a70725aba Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Tue, 15 Dec 2020 19:27:20 +0800 Subject: [PATCH] refactor: validateAndProcessEncryptSubmission to typescript (#887) * chore: use correct type for validators * refactor: migrate validateAndProcessEncryptSubmission to ts * refactor: update tests --- .../email-submission.types.ts | 1 + .../encrypt-submission.middleware.ts | 70 +++++++++++++++++++ .../encrypt-submission.utils.ts | 27 ++++++- .../modules/submission/submission.types.ts | 1 + src/app/routes/admin-forms.server.routes.js | 5 +- src/app/utils/encryption.ts | 2 +- .../field-validation/validators/common.ts | 4 +- .../validators/dateValidator.ts | 4 +- .../validators/decimalValidator.ts | 4 +- .../validators/homeNoValidator.ts | 4 +- .../validators/mobileNoValidator.ts | 4 +- .../validators/nricValidator.ts | 4 +- .../validators/radioButtonValidator.ts | 4 +- .../validators/ratingValidator.ts | 4 +- .../validators/textValidator.ts | 4 +- src/types/express.locals.ts | 6 ++ src/types/response/index.ts | 3 +- ...rypt-submissions.server.controller.spec.js | 5 +- 18 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts diff --git a/src/app/modules/submission/email-submission/email-submission.types.ts b/src/app/modules/submission/email-submission/email-submission.types.ts index 90aacf358b..bbc34f768a 100644 --- a/src/app/modules/submission/email-submission/email-submission.types.ts +++ b/src/app/modules/submission/email-submission/email-submission.types.ts @@ -39,6 +39,7 @@ export interface EmailDataForOneField { // When a response has been formatted for email, all answerArray // should have been converted to answer interface IResponseFormattedForEmail extends IBaseResponse { + question: string fieldType: BasicField answer: string } diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts new file mode 100644 index 0000000000..9f9011f39c --- /dev/null +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.middleware.ts @@ -0,0 +1,70 @@ +import { RequestHandler } from 'express' +import { SetOptional } from 'type-fest' + +import { createReqMeta } from '../../../../app/utils/request' +import { createLoggerWithLabel } from '../../../../config/logger' +import { FieldResponse, WithForm, WithParsedResponses } from '../../../../types' +import { checkIsEncryptedEncoding } from '../../../utils/encryption' +import { getProcessedResponses } from '../submission.service' + +import { mapRouteError } from './encrypt-submission.utils' + +const logger = createLoggerWithLabel(module) + +type EncryptSubmissionBody = { + responses: FieldResponse[] + encryptedContent: string + attachments?: { + encryptedFile?: { + binary: string + nonce: string + submissionPublicKey: string + } + } + isPreview: boolean + version: number +} + +export const validateAndProcessEncryptSubmission: RequestHandler< + { formId: string }, + unknown, + EncryptSubmissionBody +> = (req, res, next) => { + const { form } = req as WithForm + const { encryptedContent, responses } = req.body + + // Step 1: Check whether submitted encryption is valid. + return ( + checkIsEncryptedEncoding(encryptedContent) + // Step 2: Encryption is valid, process given responses. + .andThen(() => getProcessedResponses(form, responses)) + // If pass, then set parsedResponses and delete responses. + .map((processedResponses) => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;(req.body as WithParsedResponses< + typeof req.body + >).parsedResponses = processedResponses + // Prevent downstream functions from using responses by deleting it. + delete (req.body as SetOptional) + .responses + return next() + }) + // If error, log and return res error. + .mapErr((error) => { + logger.error({ + message: 'Error validating encrypt submission responses', + meta: { + action: 'validateEncryptSubmission', + ...createReqMeta(req), + formId: form._id, + }, + error, + }) + + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ + message: errorMessage, + }) + }) + ) +} diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts index 4a84921b46..6f99594cb7 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts @@ -4,7 +4,13 @@ import { createLoggerWithLabel } from '../../../../config/logger' import { MapRouteError } from '../../../../types/routing' import { DatabaseError, MalformedParametersError } from '../../core/core.errors' import { CreatePresignedUrlError } from '../../form/admin-form/admin-form.errors' -import { SubmissionNotFoundError } from '../submission.errors' +import { + ConflictError, + InvalidEncodingError, + ProcessingError, + SubmissionNotFoundError, + ValidateFieldError, +} from '../submission.errors' const logger = createLoggerWithLabel(module) @@ -25,6 +31,25 @@ export const mapRouteError: MapRouteError = (error) => { statusCode: StatusCodes.NOT_FOUND, errorMessage: error.message, } + case InvalidEncodingError: + return { + statusCode: StatusCodes.BAD_REQUEST, + errorMessage: + 'Invalid data was found. Please check your responses and submit again.', + } + case ValidateFieldError: + case ProcessingError: + return { + statusCode: StatusCodes.BAD_REQUEST, + errorMessage: + 'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.', + } + case ConflictError: + return { + statusCode: StatusCodes.CONFLICT, + errorMessage: + 'The form has been updated. Please refresh and submit again.', + } case CreatePresignedUrlError: case DatabaseError: return { diff --git a/src/app/modules/submission/submission.types.ts b/src/app/modules/submission/submission.types.ts index 20666cdf60..ede40f812c 100644 --- a/src/app/modules/submission/submission.types.ts +++ b/src/app/modules/submission/submission.types.ts @@ -6,6 +6,7 @@ import { } from 'src/types/response' export type ProcessedResponse = { + question: string isVisible?: boolean isUserVerified?: boolean } diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index 6087329886..7f43732e79 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -18,6 +18,7 @@ const EncryptSubmissionController = require('../modules/submission/encrypt-submi const { PermissionLevel, } = require('../modules/form/admin-form/admin-form.types') +const EncryptSubmissionMiddleware = require('../modules/submission/encrypt-submission/encrypt-submission.middleware') const SpcpController = require('../modules/spcp/spcp.controller') const { BasicField, ResponseMode } = require('../../types') @@ -476,8 +477,6 @@ module.exports = function (app) { * @security OTP */ app.route('/v2/submissions/encrypt/preview/:formId([a-fA-F0-9]{24})').post( - authActiveForm(PermissionLevel.Read), - encryptSubmissions.validateEncryptSubmission, celebrate({ [Segments.BODY]: Joi.object({ responses: Joi.array() @@ -522,6 +521,8 @@ module.exports = function (app) { version: Joi.number().required(), }), }), + authActiveForm(PermissionLevel.Read), + EncryptSubmissionMiddleware.validateAndProcessEncryptSubmission, AdminFormController.passThroughSpcp, submissions.injectAutoReplyInfo, webhookVerifiedContentFactory.encryptedVerifiedFields, diff --git a/src/app/utils/encryption.ts b/src/app/utils/encryption.ts index 8256a11ce7..080d469fab 100644 --- a/src/app/utils/encryption.ts +++ b/src/app/utils/encryption.ts @@ -5,7 +5,7 @@ import { InvalidEncodingError } from '../modules/submission/submission.errors' export const checkIsEncryptedEncoding = ( encryptedStr: string, -): Result => { +): Result => { // TODO (#42): Remove this type check once whole backend is in TypeScript. if (typeof encryptedStr !== 'string') { return err(new InvalidEncodingError('encryptedStr is not of type `string`')) diff --git a/src/app/utils/field-validation/validators/common.ts b/src/app/utils/field-validation/validators/common.ts index 9fc4da180f..1eddc822b4 100644 --- a/src/app/utils/field-validation/validators/common.ts +++ b/src/app/utils/field-validation/validators/common.ts @@ -1,9 +1,9 @@ import { left, right } from 'fp-ts/lib/Either' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' -export const notEmptySingleAnswerResponse: ResponseValidator = ( +export const notEmptySingleAnswerResponse: ResponseValidator = ( response, ) => { if (response.answer.trim().length === 0) diff --git a/src/app/utils/field-validation/validators/dateValidator.ts b/src/app/utils/field-validation/validators/dateValidator.ts index a9abcd380b..b2c447bb75 100644 --- a/src/app/utils/field-validation/validators/dateValidator.ts +++ b/src/app/utils/field-validation/validators/dateValidator.ts @@ -2,15 +2,15 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' import moment from 'moment-timezone' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IDateField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { DateSelectedValidation } from '../../../../shared/constants' import { notEmptySingleAnswerResponse } from './common' -type DateValidator = ResponseValidator +type DateValidator = ResponseValidator type DateValidatorConstructor = (dateField: IDateField) => DateValidator /** diff --git a/src/app/utils/field-validation/validators/decimalValidator.ts b/src/app/utils/field-validation/validators/decimalValidator.ts index cb49790dce..294b8fbd62 100644 --- a/src/app/utils/field-validation/validators/decimalValidator.ts +++ b/src/app/utils/field-validation/validators/decimalValidator.ts @@ -3,13 +3,13 @@ import { flow } from 'fp-ts/lib/function' import isFloat from 'validator/lib/isFloat' import isInt from 'validator/lib/isInt' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IDecimalField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { notEmptySingleAnswerResponse } from './common' -type DecimalValidator = ResponseValidator +type DecimalValidator = ResponseValidator type DecimalValidatorConstructor = ( decimalField: IDecimalField, ) => DecimalValidator diff --git a/src/app/utils/field-validation/validators/homeNoValidator.ts b/src/app/utils/field-validation/validators/homeNoValidator.ts index dd5696d242..fcf53078d1 100644 --- a/src/app/utils/field-validation/validators/homeNoValidator.ts +++ b/src/app/utils/field-validation/validators/homeNoValidator.ts @@ -1,9 +1,9 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IHomenoField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { isHomePhoneNumber, @@ -12,7 +12,7 @@ import { import { notEmptySingleAnswerResponse } from './common' -type HomeNoValidator = ResponseValidator +type HomeNoValidator = ResponseValidator type HomeNoValidatorConstructor = ( homeNumberField: IHomenoField, ) => HomeNoValidator diff --git a/src/app/utils/field-validation/validators/mobileNoValidator.ts b/src/app/utils/field-validation/validators/mobileNoValidator.ts index 41ade8dc62..1637882ffe 100644 --- a/src/app/utils/field-validation/validators/mobileNoValidator.ts +++ b/src/app/utils/field-validation/validators/mobileNoValidator.ts @@ -1,9 +1,9 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IMobileField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { isMobilePhoneNumber, @@ -12,7 +12,7 @@ import { import { notEmptySingleAnswerResponse } from './common' -type MobileNoValidator = ResponseValidator +type MobileNoValidator = ResponseValidator type MobileNoValidatorConstructor = ( mobileNumberField: IMobileField, ) => MobileNoValidator diff --git a/src/app/utils/field-validation/validators/nricValidator.ts b/src/app/utils/field-validation/validators/nricValidator.ts index 75e5a8f8ba..7385a5617a 100644 --- a/src/app/utils/field-validation/validators/nricValidator.ts +++ b/src/app/utils/field-validation/validators/nricValidator.ts @@ -1,14 +1,14 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { isNricValid } from '../../../../shared/util/nric-validation' import { notEmptySingleAnswerResponse } from './common' -type NricValidator = ResponseValidator +type NricValidator = ResponseValidator type NricValidatorConstructor = () => NricValidator const nricValidator: NricValidator = (response) => { diff --git a/src/app/utils/field-validation/validators/radioButtonValidator.ts b/src/app/utils/field-validation/validators/radioButtonValidator.ts index 0a6ec426e7..2664b84bf9 100644 --- a/src/app/utils/field-validation/validators/radioButtonValidator.ts +++ b/src/app/utils/field-validation/validators/radioButtonValidator.ts @@ -1,14 +1,14 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IRadioField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { notEmptySingleAnswerResponse } from './common' import { isOneOfOptions, isOtherOption } from './options' -type RadioButtonValidator = ResponseValidator +type RadioButtonValidator = ResponseValidator type RadioButtonValidatorConstructor = ( radioButtonField: IRadioField, ) => RadioButtonValidator diff --git a/src/app/utils/field-validation/validators/ratingValidator.ts b/src/app/utils/field-validation/validators/ratingValidator.ts index 2aa40d4cad..69b6c6b6fd 100644 --- a/src/app/utils/field-validation/validators/ratingValidator.ts +++ b/src/app/utils/field-validation/validators/ratingValidator.ts @@ -2,13 +2,13 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' import isInt from 'validator/lib/isInt' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { IRatingField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { notEmptySingleAnswerResponse } from './common' -type RatingValidator = ResponseValidator +type RatingValidator = ResponseValidator type RatingValidatorConstructor = (ratingField: IRatingField) => RatingValidator const makeRatingLimitsValidator: RatingValidatorConstructor = (ratingField) => ( diff --git a/src/app/utils/field-validation/validators/textValidator.ts b/src/app/utils/field-validation/validators/textValidator.ts index e723a2aeb8..758e3554e8 100644 --- a/src/app/utils/field-validation/validators/textValidator.ts +++ b/src/app/utils/field-validation/validators/textValidator.ts @@ -1,9 +1,9 @@ import { chain, left, right } from 'fp-ts/lib/Either' import { flow } from 'fp-ts/lib/function' +import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { ILongTextField, IShortTextField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' -import { ISingleAnswerResponse } from 'src/types/response' import { TextSelectedValidation } from '../../../../types/field/baseField' @@ -11,7 +11,7 @@ import { notEmptySingleAnswerResponse } from './common' type TextFieldValidatorConstructor = ( textField: IShortTextField | ILongTextField, -) => ResponseValidator +) => ResponseValidator const minLengthValidator: TextFieldValidatorConstructor = (textField) => ( response, diff --git a/src/types/express.locals.ts b/src/types/express.locals.ts index 0c72a6af2c..5af57c4f31 100644 --- a/src/types/express.locals.ts +++ b/src/types/express.locals.ts @@ -1,5 +1,7 @@ // TODO (#42): remove these types when migrating away from middleware pattern +import { ProcessedFieldResponse } from '../app/modules/submission/submission.types' + import { IPopulatedForm } from './form' import { SpcpSession } from './spcp' @@ -7,6 +9,10 @@ export type WithForm = T & { form: IPopulatedForm } +export type WithParsedResponses = T & { + parsedResponses: ProcessedFieldResponse[] +} + export type ResWithSpcpSession = T & { locals: { spcpSession?: SpcpSession } } diff --git a/src/types/response/index.ts b/src/types/response/index.ts index 558912ab9e..00af308ee9 100644 --- a/src/types/response/index.ts +++ b/src/types/response/index.ts @@ -5,8 +5,9 @@ export type AttachmentsMap = Record export interface IBaseResponse { _id: IFieldSchema['_id'] fieldType: BasicField - question: string myInfo?: IMyInfo + // Signature exists for verifiable fields if the answer is verified. + signature?: string } export interface ISingleAnswerResponse extends IBaseResponse { diff --git a/tests/unit/backend/controllers/encrypt-submissions.server.controller.spec.js b/tests/unit/backend/controllers/encrypt-submissions.server.controller.spec.js index 895af7562e..3858c13414 100644 --- a/tests/unit/backend/controllers/encrypt-submissions.server.controller.spec.js +++ b/tests/unit/backend/controllers/encrypt-submissions.server.controller.spec.js @@ -4,6 +4,7 @@ const express = require('express') const request = require('supertest') const dbHandler = require('../helpers/db-handler') +const EncryptSubmissionsMiddleware = require('../../../../dist/backend/app/modules/submission/encrypt-submission/encrypt-submission.middleware') const User = dbHandler.makeModel('user.server.model', 'User') const Agency = dbHandler.makeModel('agency.server.model', 'Agency') @@ -141,7 +142,7 @@ describe('Encrypt Submissions Controller', () => { }) } - describe('validateEncryptSubmission', () => { + describe('validateAndProcessEncryptSubmission', () => { const app = express() beforeAll(() => { @@ -149,7 +150,7 @@ describe('Encrypt Submissions Controller', () => { .route(endpointPath) .post( injectFixtures, - Controller.validateEncryptSubmission, + EncryptSubmissionsMiddleware.validateAndProcessEncryptSubmission, sendSubmissionBack, ) })