diff --git a/docker-compose.yml b/docker-compose.yml index 0dd47c6ca4..ffb6c0124f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,6 +129,11 @@ services: - GROWTHBOOK_CLIENT_KEY # env vars for virus scanner - VIRUS_SCANNER_LAMBDA_FUNCTION_NAME=function + - WOGAA_SECRET_KEY + - WOGAA_START_ENDPOINT + - WOGAA_SUBMIT_ENDPOINT + - WOGAA_FEEDBACK_ENDPOINT + mockpass: build: https://github.com/opengovsg/mockpass.git#v4.0.4 diff --git a/package-lock.json b/package-lock.json index 25eaed6676..8157f24117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,7 @@ "uid-generator": "^2.0.0", "ulid": "^2.3.0", "uuid": "^9.0.0", + "uuid-by-string": "^4.0.0", "validator": "^13.7.0", "web-streams-polyfill": "^3.2.1", "whatwg-fetch": "^3.6.2", @@ -20309,12 +20310,22 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-md5": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", + "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" + }, "node_modules/js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", "dev": true }, + "node_modules/js-sha1": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.6.0.tgz", + "integrity": "sha512-01gwBFreYydzmU9BmZxpVk6svJJHrVxEN3IOiGl6VO93bVKYETJ0sIth6DASI6mIFdt7NmfX9UiByRzsYHGU9w==" + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -28122,6 +28133,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-by-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/uuid-by-string/-/uuid-by-string-4.0.0.tgz", + "integrity": "sha512-88ZSfcSkN04juiLqSsuyteqlSrXNFdsEPzSv3urnElDXNsZUXQN0smeTnh99x2DE15SCUQNgqKBfro54CuzHNQ==", + "dependencies": { + "js-md5": "^0.7.3", + "js-sha1": "^0.6.0" + } + }, "node_modules/v8-compile-cache": { "version": "2.1.1", "dev": true, @@ -44450,12 +44470,22 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==" }, + "js-md5": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", + "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==" + }, "js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", "dev": true }, + "js-sha1": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.6.0.tgz", + "integrity": "sha512-01gwBFreYydzmU9BmZxpVk6svJJHrVxEN3IOiGl6VO93bVKYETJ0sIth6DASI6mIFdt7NmfX9UiByRzsYHGU9w==" + }, "js-tokens": { "version": "4.0.0", "dev": true @@ -49960,6 +49990,15 @@ "uuid": { "version": "9.0.0" }, + "uuid-by-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/uuid-by-string/-/uuid-by-string-4.0.0.tgz", + "integrity": "sha512-88ZSfcSkN04juiLqSsuyteqlSrXNFdsEPzSv3urnElDXNsZUXQN0smeTnh99x2DE15SCUQNgqKBfro54CuzHNQ==", + "requires": { + "js-md5": "^0.7.3", + "js-sha1": "^0.6.0" + } + }, "v8-compile-cache": { "version": "2.1.1", "dev": true diff --git a/package.json b/package.json index cbb3f9108d..a2386c31b0 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "uid-generator": "^2.0.0", "ulid": "^2.3.0", "uuid": "^9.0.0", + "uuid-by-string": "^4.0.0", "validator": "^13.7.0", "web-streams-polyfill": "^3.2.1", "whatwg-fetch": "^3.6.2", diff --git a/src/app/config/features/wogaa.ts b/src/app/config/features/wogaa.ts new file mode 100644 index 0000000000..e5777ed153 --- /dev/null +++ b/src/app/config/features/wogaa.ts @@ -0,0 +1,39 @@ +import convict, { Schema } from 'convict' + +export interface IWogaa { + wogaaSecretKey: string + wogaaStartEndpoint: string + wogaaSubmitEndpoint: string + wogaaFeedbackEndpoint: string +} + +const wogaaSchema: Schema = { + wogaaSecretKey: { + doc: 'Wogaa shared secret key', + format: String, + default: '', + env: 'WOGAA_SECRET_KEY', + }, + wogaaStartEndpoint: { + doc: 'Wogaa endpoint when a form is loaded', + format: String, + default: '', + env: 'WOGAA_START_ENDPOINT', + }, + wogaaSubmitEndpoint: { + doc: 'Wogaa endpoint when a form is loaded', + format: String, + default: '', + env: 'WOGAA_SUBMIT_ENDPOINT', + }, + wogaaFeedbackEndpoint: { + doc: 'Wogaa endpoint when a form is loaded', + format: String, + default: '', + env: 'WOGAA_FEEDBACK_ENDPOINT', + }, +} + +export const wogaaConfig = convict(wogaaSchema) + .validate({ allowed: 'strict' }) + .getProperties() diff --git a/src/app/modules/wogaa/wogaa.controller.ts b/src/app/modules/wogaa/wogaa.controller.ts new file mode 100644 index 0000000000..1d755fcaf3 --- /dev/null +++ b/src/app/modules/wogaa/wogaa.controller.ts @@ -0,0 +1,160 @@ +import Axios from 'axios' +import * as crypto from 'crypto' +import uuidGen from 'uuid-by-string' + +import { wogaaConfig } from '../../config/features/wogaa' +import { createLoggerWithLabel } from '../../config/logger' +import { ControllerHandler } from '../core/core.types' + +const logger = createLoggerWithLabel(module) +const generateSignature = (payload: Record) => { + const signature = crypto + .createHmac('sha256', wogaaConfig.wogaaSecretKey) + .update(JSON.stringify(payload)) + .digest('hex') + return signature +} + +const isConfigValid = () => { + if (!wogaaConfig.wogaaSecretKey) { + return false + } + if (!wogaaConfig.wogaaStartEndpoint) { + return false + } + if (!wogaaConfig.wogaaSubmitEndpoint) { + return false + } + if (!wogaaConfig.wogaaFeedbackEndpoint) { + return false + } + + return true +} + +export const handleSubmit: ControllerHandler<{ formId: string }> = async ( + req, + _, + next, +) => { + const { formId } = req.params + + if (!req.sessionID || !formId || !isConfigValid()) { + return next() + } + + const logMeta = { + action: 'wogaaHandleSubmit', + formId, + } + + const payload = { + formSgId: formId, + transactionId: uuidGen(req.sessionID), + } + // fire and forget + void Axios.post(wogaaConfig.wogaaSubmitEndpoint, payload, { + headers: { + 'WOGAA-Signature': generateSignature(payload), + }, + }) + .then(() => { + logger.info({ + message: 'Successfully sent WOGAA submit endpoint', + meta: logMeta, + }) + }) + .catch((e) => { + logger.warn({ + message: 'Error sending to WOGAA submit endpoint', + meta: { ...logMeta, wogaaRespError: e }, + }) + }) + + return next() +} + +export const handleFormView: ControllerHandler<{ formId: string }> = async ( + req, + _, + next, +) => { + const { formId } = req.params + + if (!req.sessionID || !formId || !isConfigValid()) { + return next() + } + + const logMeta = { + action: 'wogaaHandleFormView', + formId, + } + const payload = { + formSgId: formId, + pageUrl: formId, + transactionId: uuidGen(req.sessionID), + } + void Axios.post(wogaaConfig.wogaaStartEndpoint, payload, { + headers: { + 'WOGAA-Signature': generateSignature(payload), + }, + }) + .then(() => { + logger.info({ + message: 'Successfully sent WOGAA load form endpoint', + meta: logMeta, + }) + }) + .catch((e) => { + logger.warn({ + message: 'Error sending to WOGAA load form endpoint', + meta: { ...logMeta, wogaaRespError: e }, + }) + }) + + return next() +} + +export const handleFormFeedback: ControllerHandler< + { formId: string }, + unknown, + { rating: number; comment: string } +> = async (req, _, next) => { + const { formId } = req.params + const { rating, comment } = req.body + + if (!req.sessionID || !formId || !isConfigValid()) { + return next() + } + + const logMeta = { + action: 'wogaaHandleFormFeedback', + formId, + } + const payload = { + formSgId: formId, + transactionId: uuidGen(req.sessionID), + rating, + comment, + } + + void Axios.post(wogaaConfig.wogaaFeedbackEndpoint, payload, { + headers: { + 'WOGAA-Signature': generateSignature(payload), + }, + }) + .then(() => { + logger.info({ + message: 'Successfully sent WOGAA form feedback endpoint', + meta: logMeta, + }) + }) + .catch((e) => { + logger.warn({ + message: 'Error sending to WOGAA form feedback endpoint', + meta: { ...logMeta, wogaaRespError: e }, + }) + }) + + return next() +} diff --git a/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts b/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts index 432cc64a76..0be6bbd705 100644 --- a/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts @@ -1,6 +1,7 @@ import { Router } from 'express' import * as FeedbackController from '../../../../modules/feedback/feedback.controller' +import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' export const PublicFormsFeedbackRouter = Router() @@ -22,4 +23,7 @@ export const PublicFormsFeedbackRouter = Router() */ PublicFormsFeedbackRouter.route( '/:formId([a-fA-F0-9]{24})/submissions/:submissionId([a-fA-F0-9]{24})/feedback', -).post(FeedbackController.handleSubmitFormFeedback) +).post( + WogaaController.handleFormFeedback, + FeedbackController.handleSubmitFormFeedback, +) diff --git a/src/app/routes/api/v3/forms/public-forms.form.routes.ts b/src/app/routes/api/v3/forms/public-forms.form.routes.ts index b14e3746e0..b72c967a0b 100644 --- a/src/app/routes/api/v3/forms/public-forms.form.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.form.routes.ts @@ -1,6 +1,7 @@ import { Router } from 'express' import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' +import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' export const PublicFormsFormRouter = Router() @@ -19,6 +20,7 @@ export const PublicFormsFormRouter = Router() * @returns 500 when database error occurs */ PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})').get( + WogaaController.handleFormView, PublicFormController.handleGetPublicForm, ) diff --git a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts index 947e9ed643..e53836ac2d 100644 --- a/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts @@ -5,6 +5,7 @@ import * as EmailSubmissionController from '../../../../modules/submission/email import * as EncryptSubmissionController from '../../../../modules/submission/encrypt-submission/encrypt-submission.controller' import * as MultirespondentSubmissionController from '../../../../modules/submission/multirespondent-submission/multirespondent-submission.controller' import * as SubmissionController from '../../../../modules/submission/submission.controller' +import * as WogaaController from '../../../../modules/wogaa/wogaa.controller' import { limitRate } from '../../../../utils/limit-rate' export const PublicFormsSubmissionsRouter = Router() @@ -29,6 +30,7 @@ PublicFormsSubmissionsRouter.route( '/:formId([a-fA-F0-9]{24})/submissions/email', ).post( limitRate({ max: rateLimitConfig.submissions }), + WogaaController.handleSubmit, EmailSubmissionController.handleEmailSubmission, ) @@ -45,6 +47,7 @@ PublicFormsSubmissionsRouter.route( '/:formId([a-fA-F0-9]{24})/submissions/storage', ).post( limitRate({ max: rateLimitConfig.submissions }), + WogaaController.handleSubmit, EncryptSubmissionController.handleStorageSubmission, ) @@ -61,6 +64,7 @@ PublicFormsSubmissionsRouter.route( '/:formId([a-fA-F0-9]{24})/submissions/multirespondent', ).post( limitRate({ max: rateLimitConfig.submissions }), + WogaaController.handleSubmit, MultirespondentSubmissionController.handleMultirespondentSubmission, )