diff --git a/.ebextensions/env-file-creation.config b/.ebextensions/env-file-creation.config index 2d6b460e14..e31140a634 100644 --- a/.ebextensions/env-file-creation.config +++ b/.ebextensions/env-file-creation.config @@ -46,6 +46,7 @@ files: aws ssm get-parameter --name "${ENV_TYPE}-ndi" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-verified-fields" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_TYPE}-webhook-verified-content" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_TYPE}-wogaa" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_SITE_NAME}-sgid" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_SITE_NAME}-payment" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env aws ssm get-parameter --name "${ENV_SITE_NAME}-cron-payment" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7c0fc83b..cc0c5958d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,24 @@ 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.112.0](https://github.com/opengovsg/FormSG/compare/v6.112.0...v6.112.0) + +- fix: add wogaa config into .env [`#7125`](https://github.com/opengovsg/FormSG/pull/7125) + +#### [v6.112.0](https://github.com/opengovsg/FormSG/compare/v6.111.0...v6.112.0) + +> 6 March 2024 + +- feat(tracking): wogaa tracking [`#7123`](https://github.com/opengovsg/FormSG/pull/7123) +- chore(deps-dev): bump json5 from 1.0.1 to 1.0.2 [`#7119`](https://github.com/opengovsg/FormSG/pull/7119) +- build: merge v6.111.0 back into develop [`#7118`](https://github.com/opengovsg/FormSG/pull/7118) +- build: release v6.111.0 [`#7117`](https://github.com/opengovsg/FormSG/pull/7117) +- chore: bump version to v6.112.0 [`54946e9`](https://github.com/opengovsg/FormSG/commit/54946e99d101d48512fce17e0c4d6f9c09db315a) + #### [v6.111.0](https://github.com/opengovsg/FormSG/compare/v6.110.0...v6.111.0) +> 4 March 2024 + - feat(fe): update copy, copy btn [`#7116`](https://github.com/opengovsg/FormSG/pull/7116) - feat(virus-scanner): allow endpoint to be specified [`#7114`](https://github.com/opengovsg/FormSG/pull/7114) - chore: remove Anguilla from country listing [`#7108`](https://github.com/opengovsg/FormSG/pull/7108) @@ -15,6 +31,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(OSS): add FerretDB migration instructions [`#7107`](https://github.com/opengovsg/FormSG/pull/7107) - build: merge v6.110.0 back into develop [`#7105`](https://github.com/opengovsg/FormSG/pull/7105) - build: release v6.110.0 [`#7104`](https://github.com/opengovsg/FormSG/pull/7104) +- chore: bump version to v6.111.0 [`d71e1bf`](https://github.com/opengovsg/FormSG/commit/d71e1bf707c8bd10d16266dc49d04979fb2cdfec) #### [v6.110.0](https://github.com/opengovsg/FormSG/compare/v6.109.0...v6.110.0) 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/frontend/package-lock.json b/frontend/package-lock.json index fbb2e7dc88..e45e01aa06 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.111.0", + "version": "6.112.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.111.0", + "version": "6.112.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", diff --git a/frontend/package.json b/frontend/package.json index b7ba9b626d..a580dd7881 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.111.0", + "version": "6.112.0", "homepage": ".", "private": true, "dependencies": { diff --git a/package-lock.json b/package-lock.json index 1ddabfdde1..3378e184a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.111.0", + "version": "6.112.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.111.0", + "version": "6.112.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.347.1", @@ -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", @@ -10490,9 +10491,10 @@ } }, "node_modules/babel-loader/node_modules/json5": { - "version": "2.2.1", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -20308,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, @@ -20694,9 +20706,10 @@ "license": "ISC" }, "node_modules/json5": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -27363,9 +27376,9 @@ } }, "node_modules/ts-loader/node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -28120,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, @@ -37610,7 +37632,9 @@ } }, "json5": { - "version": "2.2.1", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "loader-utils": { @@ -44446,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 @@ -44729,7 +44763,9 @@ "version": "5.0.1" }, "json5": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -49467,9 +49503,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "loader-utils": { @@ -49954,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..bc0e74f399 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.111.0", + "version": "6.112.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -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, )