diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e99ad9866..8960325fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,49 @@ 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). -#### [v5.4.1](https://github.com/opengovsg/FormSG/compare/v5.4.0...v5.4.1) - +#### [v5.5.0](https://github.com/opengovsg/FormSG/compare/v5.4.0...v5.5.0) + +- [Snyk] Security upgrade mongoose from 5.11.10 to 5.12.3 [`#1538`](https://github.com/opengovsg/FormSG/pull/1538) +- ref(auth-api): duplicate auth endpoints to new /api/v3 router [`#1551`](https://github.com/opengovsg/FormSG/pull/1551) +- chore: add createReqMeta to verification module logging [`#1562`](https://github.com/opengovsg/FormSG/pull/1562) +- chore(deps-dev): bump @typescript-eslint/parser from 4.20.0 to 4.21.0 [`#1558`](https://github.com/opengovsg/FormSG/pull/1558) +- chore(deps-dev): bump ngrok from 4.0.0 to 4.0.1 [`#1560`](https://github.com/opengovsg/FormSG/pull/1560) +- chore(deps-dev): bump eslint-plugin-jest from 24.3.3 to 24.3.4 [`#1559`](https://github.com/opengovsg/FormSG/pull/1559) +- chore(deps-dev): bump @types/mongodb from 3.6.10 to 3.6.12 [`#1557`](https://github.com/opengovsg/FormSG/pull/1557) +- chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1556`](https://github.com/opengovsg/FormSG/pull/1556) +- fix(deps): bump @opengovsg/spcp-auth-client from 1.4.4 to 1.4.5 [`#1555`](https://github.com/opengovsg/FormSG/pull/1555) +- chore(deps): remove nodemailer-direct-transport, not used [`#1550`](https://github.com/opengovsg/FormSG/pull/1550) +- refactor: convert submission limit service function to ResultAsync [`#1539`](https://github.com/opengovsg/FormSG/pull/1539) +- fix(deps): bump aws-sdk from 2.877.0 to 2.879.0 [`#1549`](https://github.com/opengovsg/FormSG/pull/1549) +- chore(deps-dev): bump eslint-plugin-jest from 24.3.2 to 24.3.3 [`#1548`](https://github.com/opengovsg/FormSG/pull/1548) +- fix(deps): bump @sentry/browser from 6.2.4 to 6.2.5 [`#1545`](https://github.com/opengovsg/FormSG/pull/1545) +- fix(deps): bump aws-sdk from 2.876.0 to 2.877.0 [`#1546`](https://github.com/opengovsg/FormSG/pull/1546) +- fix(deps): bump @sentry/integrations from 6.2.4 to 6.2.5 [`#1543`](https://github.com/opengovsg/FormSG/pull/1543) +- chore: merge release 5.4.1 back to develop [`#1542`](https://github.com/opengovsg/FormSG/pull/1542) +- ref: collapse middlewares of GET /adminform/submissions/metadata route [`#1526`](https://github.com/opengovsg/FormSG/pull/1526) +- ref: collapse middlewares of GET /:formId/adminform/submissions route [`#1530`](https://github.com/opengovsg/FormSG/pull/1530) +- fix(deps): bump aws-sdk from 2.874.0 to 2.876.0 [`#1527`](https://github.com/opengovsg/FormSG/pull/1527) +- fix(deps): bump @sentry/browser from 6.2.3 to 6.2.4 [`#1525`](https://github.com/opengovsg/FormSG/pull/1525) +- fix(deps): bump @sentry/integrations from 6.2.3 to 6.2.4 [`#1523`](https://github.com/opengovsg/FormSG/pull/1523) +- refactor: use neverthrow in /:transactionId/otp/verify [`#1455`](https://github.com/opengovsg/FormSG/pull/1455) +- chore: merge v5.4.0 into develop [`#1505`](https://github.com/opengovsg/FormSG/pull/1505) - build: release v5.4.0 [`#1493`](https://github.com/opengovsg/FormSG/pull/1493) +- refactor: use neverthrow in /:transactionId/otp (part 5) [`#1454`](https://github.com/opengovsg/FormSG/pull/1454) +- refactor: use neverthrow in /:transactionId/reset (part 4) [`#1453`](https://github.com/opengovsg/FormSG/pull/1453) +- refactor: use neverthrow in /transaction (part 3) [`#1452`](https://github.com/opengovsg/FormSG/pull/1452) +- chore(deps-dev): bump husky from 5.2.0 to 6.0.0 [`#1477`](https://github.com/opengovsg/FormSG/pull/1477) +- chore(deps-dev): bump ngrok from 3.4.1 to 4.0.0 [`#1469`](https://github.com/opengovsg/FormSG/pull/1469) +- chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1480`](https://github.com/opengovsg/FormSG/pull/1480) +- refactor: Migrate the rest of the filters to handleEncryptedSubmission [`#1474`](https://github.com/opengovsg/FormSG/pull/1474) +- chore(deps-dev): bump @typescript-eslint/parser from 4.19.0 to 4.20.0 [`#1479`](https://github.com/opengovsg/FormSG/pull/1479) +- chore(deps-dev): bump @babel/core from 7.13.13 to 7.13.14 [`#1478`](https://github.com/opengovsg/FormSG/pull/1478) +- refactor: use neverthrow in /transaction/:transactionId [`#1451`](https://github.com/opengovsg/FormSG/pull/1451) - build: release v5.3.0 [`#1430`](https://github.com/opengovsg/FormSG/pull/1430) - build: release v5.2.0 [`#1381`](https://github.com/opengovsg/FormSG/pull/1381) - build: release 5.1.0 [`#1337`](https://github.com/opengovsg/FormSG/pull/1337) - build: release 5.0.4 - undefined checks for MyInfo address fields [`#1249`](https://github.com/opengovsg/FormSG/pull/1249) - build: Release v5.0.3 - protect against registered address field type bug [`#1247`](https://github.com/opengovsg/FormSG/pull/1247) +- build: release 5.4.1 - server to respond with status code if storage mode form is archived [`7fb57ad`](https://github.com/opengovsg/FormSG/commit/7fb57ad56629862daa2fb2d0a490bbed4b6e139e) - fix: server to respond with status code if storage mode form is archived [`91551a9`](https://github.com/opengovsg/FormSG/commit/91551a936de5c09b3193689b65929697ae2e6108) #### [v5.4.0](https://github.com/opengovsg/FormSG/compare/v5.3.0...v5.4.0) @@ -131,7 +166,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build: merge release 5.1.0 into develop [`#1341`](https://github.com/opengovsg/FormSG/pull/1341) - chore: bump version to 5.2.0 [`0eca821`](https://github.com/opengovsg/FormSG/commit/0eca8214f75345c76915ee8c14f342316dd1f190) -#### [v5.1.0](https://github.com/opengovsg/FormSG/compare/v5.0.3...v5.1.0) +### [v5.1.0](https://github.com/opengovsg/FormSG/compare/v4.59.1...v5.1.0) > 10 March 2021 @@ -204,34 +239,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: Add submission limits for storage mode form submissions [`#1097`](https://github.com/opengovsg/FormSG/pull/1097) - chore: merge v5.0.4 into develop [`#1250`](https://github.com/opengovsg/FormSG/pull/1250) - chore: merge Release v5.0.3 back to develop branch [`#1248`](https://github.com/opengovsg/FormSG/pull/1248) -- chore: merge v5.0.1 into develop [`#1241`](https://github.com/opengovsg/FormSG/pull/1241) - fix: format workpass status correctly, preview submissions, copy changes [`#1237`](https://github.com/opengovsg/FormSG/pull/1237) - feat: update editable fields and clear cookie upon submission [`#1232`](https://github.com/opengovsg/FormSG/pull/1232) - style: update end page styling [`#1231`](https://github.com/opengovsg/FormSG/pull/1231) - feat: migrate to MyInfo V3 [`#1175`](https://github.com/opengovsg/FormSG/pull/1175) -- chore: merge v4.59.1 into develop [`#1227`](https://github.com/opengovsg/FormSG/pull/1227) -- chore: bump version to 5.1.0 [`8bb5ad1`](https://github.com/opengovsg/FormSG/commit/8bb5ad107e224d6957d873b34e8646c1d3f7f477) -- chore: bump version to 5.0.4 [`9a422f8`](https://github.com/opengovsg/FormSG/commit/9a422f8fceb2a608f4983306376b54bd79cb8f77) -- fix: check truthiness for all address fields [`bad7228`](https://github.com/opengovsg/FormSG/commit/bad7228c631d274e7ff91063d2c3b6d3b88bb497) - -#### [v5.0.3](https://github.com/opengovsg/FormSG/compare/v5.0.2...v5.0.3) - -> 25 February 2021 - -- fix: format workpass status correctly, preview submissions, copy changes [`#1237`](https://github.com/opengovsg/FormSG/pull/1237) -- feat: update editable fields and clear cookie upon submission [`#1232`](https://github.com/opengovsg/FormSG/pull/1232) -- style: update end page styling [`#1231`](https://github.com/opengovsg/FormSG/pull/1231) -- feat: migrate to MyInfo V3 [`#1175`](https://github.com/opengovsg/FormSG/pull/1175) -- chore: bump version to v5.0.2 [`256d772`](https://github.com/opengovsg/FormSG/commit/256d772f35d69a535a8062e36a6b2c1e5609784e) -- chore: bump version to 5.0.0 [`e411cf3`](https://github.com/opengovsg/FormSG/commit/e411cf39f69e676892849e8f3ed1cc0d8ebdcf2f) -- chore: bump version to 5.0.1 [`a235730`](https://github.com/opengovsg/FormSG/commit/a2357301933f30edbb3952c43e4c9e4395751e3c) - -### [v5.0.2](https://github.com/opengovsg/FormSG/compare/v4.59.1...v5.0.2) - -> 25 February 2021 - -- fix: convert MyInfo residential status to uppercase [`#1238`](https://github.com/opengovsg/FormSG/pull/1238) -- build: release v5.0.0 - migrate to MyInfo V3 [`#1228`](https://github.com/opengovsg/FormSG/pull/1228) +- chore: merge v5.0.1 into develop [`#1241`](https://github.com/opengovsg/FormSG/pull/1241) - fix: format workpass status correctly, preview submissions, copy changes [`#1237`](https://github.com/opengovsg/FormSG/pull/1237) - feat: update editable fields and clear cookie upon submission [`#1232`](https://github.com/opengovsg/FormSG/pull/1232) - style: update end page styling [`#1231`](https://github.com/opengovsg/FormSG/pull/1231) @@ -288,9 +300,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Release 4.30.2 - fix AWS endpoint and /emailnotifications log group [`#78`](https://github.com/opengovsg/FormSG/pull/78) - build: Release 4.30.1 - Fix field creation on old clients [`#74`](https://github.com/opengovsg/FormSG/pull/74) - Release 4.30.0 - acknowledgement for secret key backup, TypeScript migrations [`#67`](https://github.com/opengovsg/FormSG/pull/67) -- build: release v5.0.2 - registered address value undefined bug [`dbcdbf0`](https://github.com/opengovsg/FormSG/commit/dbcdbf09cc4ad8f23408a450bfadcc602c0bf20e) -- chore: bump version to 5.0.0 [`fb11aee`](https://github.com/opengovsg/FormSG/commit/fb11aee0d19214d824a350c02a3e2a04f89ea726) -- fix(registered-address): test truthiness of all value property accessors [`bd6ad34`](https://github.com/opengovsg/FormSG/commit/bd6ad3494c42432ec60fa135bb0067b41fa0137a) +- chore: bump version to 5.1.0 [`8bb5ad1`](https://github.com/opengovsg/FormSG/commit/8bb5ad107e224d6957d873b34e8646c1d3f7f477) +- chore: bump version to v5.0.2 [`256d772`](https://github.com/opengovsg/FormSG/commit/256d772f35d69a535a8062e36a6b2c1e5609784e) +- chore: bump version to 5.0.4 [`9a422f8`](https://github.com/opengovsg/FormSG/commit/9a422f8fceb2a608f4983306376b54bd79cb8f77) #### [v4.59.1](https://github.com/opengovsg/FormSG/compare/v4.59.0...v4.59.1) @@ -671,16 +683,16 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build: Release 4.30.1 - Fix field creation on old clients [`#74`](https://github.com/opengovsg/FormSG/pull/74) - Release 4.30.0 - acknowledgement for secret key backup, TypeScript migrations [`#67`](https://github.com/opengovsg/FormSG/pull/67) - build: empty commit to trigger PR build [`d0c6583`](https://github.com/opengovsg/FormSG/commit/d0c65838efa6731a0f10e89ff954c132b9f8b854) +- feat: update table field styling to not rely on multiple divs [`db03da3`](https://github.com/opengovsg/FormSG/commit/db03da33eca2fc0a6174ca58d7dd8b3cfadadcea) - fix: return 401 for missing JWT [`e6c1947`](https://github.com/opengovsg/FormSG/commit/e6c19477b05fc4aed90b2db42916220aea2a263c) -- test: add tests for extractJwt [`16191a9`](https://github.com/opengovsg/FormSG/commit/16191a957be0dc7f63dc4221569fac2794b7d063) #### [v4.50.2](https://github.com/opengovsg/FormSG/compare/v4.50.1...v4.50.2) > 16 December 2020 -- feat: update table field styling to not rely on multiple divs [`db03da3`](https://github.com/opengovsg/FormSG/commit/db03da33eca2fc0a6174ca58d7dd8b3cfadadcea) +- feat: update table field styling to not rely on multiple divs [`e857aed`](https://github.com/opengovsg/FormSG/commit/e857aed667145441758f09f311c2b9f16f57645b) - fix: email format validation should allow 126/163.com, align frontend and backend validation [`be35522`](https://github.com/opengovsg/FormSG/commit/be35522e15d20521f58fada7ed3164e6c0c0894d) -- chore: bump version to v4.50.2 [`1ac7be6`](https://github.com/opengovsg/FormSG/commit/1ac7be6cb69553d186ec194682ae25bd98318a8b) +- chore: bump version to v4.50.2 [`fad62ec`](https://github.com/opengovsg/FormSG/commit/fad62ec5730412201e208332a91286fd53e2b36a) #### [v4.50.1](https://github.com/opengovsg/FormSG/compare/v4.50.0...v4.50.1) @@ -910,7 +922,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### [v4.46.0](https://github.com/opengovsg/FormSG/compare/v4.45.0...v4.46.0) -> 17 November 2020 +> 18 November 2020 - chore: use travis_retry to retry flaky tests automatically [`#641`](https://github.com/opengovsg/FormSG/pull/641) - refactor: migrate MyInfo functionality to TypeScript [`#560`](https://github.com/opengovsg/FormSG/pull/560) @@ -952,7 +964,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - ref: migrate inline queries for retrieval of submissions metadata to model static methods [`#601`](https://github.com/opengovsg/FormSG/pull/601) - chore: bump version to v4.46.0 [`bcdcf03`](https://github.com/opengovsg/FormSG/commit/bcdcf03afdbffb3562386129c793dee5b459b321) - chore: bump version to 4.45.1 [`d2ec536`](https://github.com/opengovsg/FormSG/commit/d2ec536e5154c9459b97d6977724928dc68e2a19) -- fix: update email list correctly when deleting emails [`5776c2d`](https://github.com/opengovsg/FormSG/commit/5776c2d3d079e2876aacb17c41c398031a8ff939) +- fix: add minimal polyfill for ie11 in decryption worker [`a9d2068`](https://github.com/opengovsg/FormSG/commit/a9d2068d70c61c09910599a37bb8d5e14ead2ffc) #### [v4.45.0](https://github.com/opengovsg/FormSG/compare/v4.44.0...v4.45.0) @@ -1003,9 +1015,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump eslint-config-prettier from 6.12.0 to 6.15.0 [`#526`](https://github.com/opengovsg/FormSG/pull/526) - build: merge release 4.43.2 into develop [`#552`](https://github.com/opengovsg/FormSG/pull/552) - build: release 4.43.2 [`#551`](https://github.com/opengovsg/FormSG/pull/551) -- fix(prefill): implement flag to restrict prefillable form fields and disallow prefilling for myinfo fields [`#550`](https://github.com/opengovsg/FormSG/pull/550) -- fix(prefill): do not disable textfield due to phishing concerns [`#540`](https://github.com/opengovsg/FormSG/pull/540) -- build: merge Release 4.43.0 to develop [`#537`](https://github.com/opengovsg/FormSG/pull/537) - build: Release 4.43.0 [`#529`](https://github.com/opengovsg/FormSG/pull/529) - build: Release 4.42.0 [`#518`](https://github.com/opengovsg/FormSG/pull/518) - build: Release 4.41.0 [`#493`](https://github.com/opengovsg/FormSG/pull/493) @@ -1029,17 +1038,16 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build: Release 4.30.1 - Fix field creation on old clients [`#74`](https://github.com/opengovsg/FormSG/pull/74) - Release 4.30.0 - acknowledgement for secret key backup, TypeScript migrations [`#67`](https://github.com/opengovsg/FormSG/pull/67) - chore: bump version to 4.44.0 [`03f24e4`](https://github.com/opengovsg/FormSG/commit/03f24e4124ad0aefd54f9360354ec2e4bd7be14e) -- chore: bump version to v4.43.2 [`1a79111`](https://github.com/opengovsg/FormSG/commit/1a7911123004b437f38008e267a9e324729aa5ed) -- build: merge 4.43.1 to develop branch [`6942881`](https://github.com/opengovsg/FormSG/commit/694288184b920a60a7a1cd3baf2bd66dc42bb0fc) +- build: Release 4.43.1 - Allow edits to prefilled textfields, tighten checks for protected routes [`b55e5d5`](https://github.com/opengovsg/FormSG/commit/b55e5d5be4412a620feefc52bf360914c33a8bc7) +- build: Merge pull request #348 from opengovsg/release-4.36.0 [`211efe7`](https://github.com/opengovsg/FormSG/commit/211efe7aac19d9b5b7b18af67f3707f47d138f96) #### [v4.43.2](https://github.com/opengovsg/FormSG/compare/v4.43.1...v4.43.2) -> 29 October 2020 +> 30 October 2020 -- fix(prefill): do not disable textfield due to phishing concerns [`#540`](https://github.com/opengovsg/FormSG/pull/540) -- build: release 4.43.1 [`b4599b8`](https://github.com/opengovsg/FormSG/commit/b4599b8a05498c4dd2eb86a513de056570d864cc) -- chore: bump version to v4.43.0 [`415162d`](https://github.com/opengovsg/FormSG/commit/415162db717192ab97dadbfbe2d6716483d28404) -- build: release 4.43.1 [`9d1a96b`](https://github.com/opengovsg/FormSG/commit/9d1a96b45322953c35c67ffdd4dce120e83d8e62) +- fix(prefill): implement flag to restrict prefillable form fields and disallow prefilling for myinfo fields [`#550`](https://github.com/opengovsg/FormSG/pull/550) +- chore: bump version to v4.43.2 [`1a79111`](https://github.com/opengovsg/FormSG/commit/1a7911123004b437f38008e267a9e324729aa5ed) +- build: merge 4.43.1 to develop branch [`6942881`](https://github.com/opengovsg/FormSG/commit/694288184b920a60a7a1cd3baf2bd66dc42bb0fc) #### [v4.43.1](https://github.com/opengovsg/FormSG/compare/v4.43.0...v4.43.1) diff --git a/package-lock.json b/package-lock.json index bfbdff1a51..63dd73cd2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "5.4.1", + "version": "5.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4371,9 +4371,9 @@ "integrity": "sha512-YqR6GIsum9K7Cg6wOTxwJnKP+KDOxbZ9dnQE2/M47vP0ynXyTadvwflGBukzJ/MhzrS2R6buNhFjFnVJRXJinw==" }, "@opengovsg/spcp-auth-client": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@opengovsg/spcp-auth-client/-/spcp-auth-client-1.4.4.tgz", - "integrity": "sha512-GVLZphx2/9W36ZZ7nNmb/Zu/QxQbUYDC1oY0NB19IBfzlOV7FR6foqRn/6F2o9zMtC5r13nhHvxC78Vz5eMDow==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@opengovsg/spcp-auth-client/-/spcp-auth-client-1.4.5.tgz", + "integrity": "sha512-bvSNTW+2CL6gAprXSoEtkFv/9mFp6dwm0iCb9Ns56BBXy0+O/lUKI2jXZ7rBnVwXOCYtlEWB9BIqJmMLJKeOzA==", "requires": { "base-64": "^1.0.0", "jsonwebtoken": "^8.3.0", @@ -4930,9 +4930,9 @@ "dev": true }, "@types/mongodb": { - "version": "3.6.10", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.10.tgz", - "integrity": "sha512-BkwAHFiZSSWdTIqbUVGmgvIsiXXjqAketeK7Izy7oSs6G3N8Bn993tK9eq6QEovQDx6OQ2FGP2KWDDxBzdlJ6Q==", + "version": "3.6.12", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.12.tgz", + "integrity": "sha512-49aEzQD5VdHPxyd5dRyQdqEveAg9LanwrH8RQipnMuulwzKmODXIZRp0umtxi1eBUfEusRkoy8AVOMr+kVuFog==", "requires": { "@types/bson": "*", "@types/node": "*" @@ -5159,13 +5159,13 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.20.0.tgz", - "integrity": "sha512-sw+3HO5aehYqn5w177z2D82ZQlqHCwcKSMboueo7oE4KU9QiC0SAgfS/D4z9xXvpTc8Bt41Raa9fBR8T2tIhoQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz", + "integrity": "sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.20.0", - "@typescript-eslint/scope-manager": "4.20.0", + "@typescript-eslint/experimental-utils": "4.21.0", + "@typescript-eslint/scope-manager": "4.21.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", @@ -5175,43 +5175,43 @@ }, "dependencies": { "@typescript-eslint/experimental-utils": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz", - "integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz", + "integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.20.0", - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/typescript-estree": "4.20.0", + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/scope-manager": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", - "integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz", + "integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0" + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0" } }, "@typescript-eslint/types": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", - "integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz", + "integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", - "integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz", + "integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -5220,12 +5220,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", - "integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz", + "integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/types": "4.21.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -5271,55 +5271,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz", - "integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz", + "integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.20.0", - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/typescript-estree": "4.20.0", + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.20.0.tgz", - "integrity": "sha512-m6vDtgL9EABdjMtKVw5rr6DdeMCH3OA1vFb0dAyuZSa3e5yw1YRzlwFnm9knma9Lz6b2GPvoNSa8vOXrqsaglA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz", + "integrity": "sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.20.0", - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/typescript-estree": "4.20.0", + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", "debug": "^4.1.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", - "integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz", + "integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0" + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0" } }, "@typescript-eslint/types": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", - "integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz", + "integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", - "integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz", + "integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -5328,12 +5328,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", - "integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz", + "integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/types": "4.21.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -5379,29 +5379,29 @@ } }, "@typescript-eslint/scope-manager": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", - "integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz", + "integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0" + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0" } }, "@typescript-eslint/types": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", - "integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz", + "integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", - "integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz", + "integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", - "@typescript-eslint/visitor-keys": "4.20.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -5445,12 +5445,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", - "integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz", + "integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==", "dev": true, "requires": { - "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/types": "4.21.0", "eslint-visitor-keys": "^2.0.0" }, "dependencies": { @@ -10835,9 +10835,9 @@ } }, "eslint-plugin-jest": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.3.tgz", - "integrity": "sha512-IQ9tLHyKEyBw1BM3IE13WxOXQm03h/7dy1KFknUVkoY2N2+Hw7lb/3YFz/4jwcrxXt2+KhA/GoiK7jt8aK19ww==", + "version": "24.3.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.4.tgz", + "integrity": "sha512-3n5oY1+fictanuFkTWPwSlehugBTAgwLnYLFsCllzE3Pl1BwywHl5fL0HFxmMjoQY8xhUDk8uAWc3S4JOHGh3A==", "dev": true, "requires": { "@typescript-eslint/experimental-utils": "^4.0.1" @@ -17433,17 +17433,17 @@ "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=" }, "mongoose": { - "version": "5.11.10", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.10.tgz", - "integrity": "sha512-daE2L6VW7WNywv7tL2KUkBViWvODbzr50Of1kJpIbzW3w3N5/TYcgSmhCsEDWfYGQXbun2rdd7+sOdsEC8zQSQ==", + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.12.3.tgz", + "integrity": "sha512-frsSR9yeldaRpSUeTegXCSB0Tu5UGq8sHuHBuEV31Jk3COyxlKFQPL7UsdMhxPUCmk74FpOYSmNwxhWBEqgzQg==", "requires": { "@types/mongodb": "^3.5.27", "bson": "^1.1.4", "kareem": "2.3.2", - "mongodb": "3.6.3", + "mongodb": "3.6.5", "mongoose-legacy-pluralize": "1.0.2", "mpath": "0.8.3", - "mquery": "3.2.3", + "mquery": "3.2.5", "ms": "2.1.2", "regexp-clone": "1.0.0", "safe-buffer": "5.2.1", @@ -17452,14 +17452,14 @@ }, "dependencies": { "bson": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" }, "mongodb": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", - "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.5.tgz", + "integrity": "sha512-mQlYKw1iGbvJJejcPuyTaytq0xxlYbIoVDm2FODR+OHxyEiMR021vc32bTvamgBjCswsD54XIRwhg3yBaWqJjg==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -17543,9 +17543,9 @@ "integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==" }, "mquery": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz", - "integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz", + "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==", "requires": { "bluebird": "3.5.1", "debug": "3.1.0", @@ -17696,9 +17696,9 @@ } }, "ngrok": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-4.0.0.tgz", - "integrity": "sha512-fECZeX/gSHnk+Re+ycIK0SDKeTV0G86mxc6n2hbqMNwqmxYqh0lfInjZG+QhsPyAOWiIsAteydP/jFPF/WIWCA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-4.0.1.tgz", + "integrity": "sha512-1RDEaP6urGt8dVzZ68mlf1799VXucJ3bEcNDOSwWoGUjgXS7MxMw+ngyw/2H/O+EMk1Fj1+Md2Au595rB4lG5w==", "dev": true, "requires": { "@types/node": "^8.10.50", diff --git a/package.json b/package.json index 48a4d8f487..29ecad5b0d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "5.4.1", + "version": "5.5.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -63,7 +63,7 @@ "@opengovsg/formsg-sdk": "^0.8.4-beta.0", "@opengovsg/myinfo-gov-client": "=4.0.0-beta.0", "@opengovsg/ng-file-upload": "^12.2.15", - "@opengovsg/spcp-auth-client": "^1.4.4", + "@opengovsg/spcp-auth-client": "^1.4.5", "@sentry/browser": "^6.2.5", "@sentry/integrations": "^6.2.5", "@stablelib/base64": "^1.0.0", @@ -125,7 +125,7 @@ "lodash": "^4.17.21", "moment-timezone": "0.5.33", "mongodb-uri": "^0.9.7", - "mongoose": "^5.11.10", + "mongoose": "^5.12.3", "multiparty": ">=4.2.2", "neverthrow": "^4.2.1", "ng-infinite-scroll": "^1.3.0", @@ -179,7 +179,7 @@ "@types/ip": "^1.1.0", "@types/jest": "^26.0.22", "@types/json-stringify-safe": "^5.0.0", - "@types/mongodb": "^3.6.10", + "@types/mongodb": "^3.6.12", "@types/mongodb-uri": "^0.9.0", "@types/node": "^14.14.37", "@types/nodemailer": "^6.4.1", @@ -192,8 +192,8 @@ "@types/uid-generator": "^2.0.2", "@types/uuid": "^8.3.0", "@types/validator": "^13.1.3", - "@typescript-eslint/eslint-plugin": "^4.20.0", - "@typescript-eslint/parser": "^4.20.0", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", "auto-changelog": "^2.2.1", "axios-mock-adapter": "^1.19.0", "babel-loader": "^8.2.2", @@ -209,7 +209,7 @@ "eslint-config-prettier": "^8.1.0", "eslint-plugin-angular": "^4.0.1", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.3", + "eslint-plugin-jest": "^24.3.4", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-typesafe": "^0.5.2", @@ -231,7 +231,7 @@ "mockdate": "^3.0.5", "mockingoose": "^2.13.2", "mongodb-memory-server-core": "^6.9.6", - "ngrok": "^4.0.0", + "ngrok": "^4.0.1", "optimize-css-assets-webpack-plugin": "^5.0.1", "prettier": "^2.2.1", "proxyquire": "^2.1.3", diff --git a/src/app/models/admin_verification.server.model.ts b/src/app/models/admin_verification.server.model.ts index cbce56da2c..3f5d675999 100644 --- a/src/app/models/admin_verification.server.model.ts +++ b/src/app/models/admin_verification.server.model.ts @@ -12,7 +12,10 @@ import { USER_SCHEMA_ID } from './user.server.model' export const ADMIN_VERIFICATION_SCHEMA_ID = 'AdminVerification' -const AdminVerificationSchema = new Schema( +const AdminVerificationSchema = new Schema< + IAdminVerificationSchema, + IAdminVerificationModel +>( { admin: { type: Schema.Types.ObjectId, diff --git a/src/app/models/form.server.model.ts b/src/app/models/form.server.model.ts index aa8b879aed..460cb5765b 100644 --- a/src/app/models/form.server.model.ts +++ b/src/app/models/form.server.model.ts @@ -131,7 +131,7 @@ const EncryptedFormSchema = new Schema({ }, }) -const EmailFormSchema = new Schema({ +const EmailFormSchema = new Schema({ emails: { type: [ { @@ -159,7 +159,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { const User = getUserModel(db) // Schema - const FormSchema = new Schema( + const FormSchema = new Schema( { title: { type: String, @@ -342,7 +342,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { // Add discriminators for the various field types. const FormFieldPath = FormSchema.path( 'form_fields', - ) as Schema.Types.DocumentArray + ) as Schema.Types.DocumentArrayWithLooseDiscriminator const TableFieldSchema = createTableFieldSchema() @@ -376,7 +376,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { FormFieldPath.discriminator(BasicField.Table, TableFieldSchema) const TableColumnPath = TableFieldSchema.path( 'columns', - ) as Schema.Types.DocumentArray + ) as Schema.Types.DocumentArrayWithLooseDiscriminator TableColumnPath.discriminator( BasicField.ShortText, createShortTextFieldSchema(), @@ -389,13 +389,13 @@ const compileFormModel = (db: Mongoose): IFormModel => { // Discriminator defines all possible values of startPage.logo const StartPageLogoPath = FormSchema.path( 'startPage.logo', - ) as Schema.Types.DocumentArray + ) as Schema.Types.DocumentArrayWithLooseDiscriminator StartPageLogoPath.discriminator(FormLogoState.Custom, CustomFormLogoSchema) // Discriminator defines different logic types const FormLogicPath = FormSchema.path( 'form_logics', - ) as Schema.Types.DocumentArray + ) as Schema.Types.DocumentArrayWithLooseDiscriminator FormLogicPath.discriminator(LogicType.ShowFields, ShowFieldsLogicSchema) FormLogicPath.discriminator(LogicType.PreventSubmit, PreventSubmitLogicSchema) @@ -458,12 +458,6 @@ const compileFormModel = (db: Mongoose): IFormModel => { } } - FormSchema.methods.getSettings = function ( - this: IFormDocument, - ): FormSettings { - return pick(this, FORM_SETTING_FIELDS) - } - // Archives form. FormSchema.methods.archive = function (this: IFormSchema) { // Return instantly when form is already archived. @@ -475,8 +469,16 @@ const compileFormModel = (db: Mongoose): IFormModel => { return this.save() } + const FormDocumentSchema = (FormSchema as unknown) as Schema + + FormDocumentSchema.methods.getSettings = function ( + this: IFormDocument, + ): FormSettings { + return pick(this, FORM_SETTING_FIELDS) + } + // Transfer ownership of the form to another user - FormSchema.methods.transferOwner = async function ( + FormDocumentSchema.methods.transferOwner = async function ( this: IFormDocument, currentOwner: IUserSchema, newOwner: IUserSchema, diff --git a/src/app/models/form_statistics_total.server.model.ts b/src/app/models/form_statistics_total.server.model.ts index 573c61effb..212eaec6b7 100644 --- a/src/app/models/form_statistics_total.server.model.ts +++ b/src/app/models/form_statistics_total.server.model.ts @@ -12,7 +12,10 @@ const FORM_STATS_TOTAL_SCHEMA_ID = 'FormStatisticsTotal' const FORM_STATS_COLLECTION_NAME = 'formStatisticsTotal' const compileFormStatisticsTotalModel = (db: Mongoose) => { - const FormStatisticsTotalSchema = new Schema( + const FormStatisticsTotalSchema = new Schema< + IFormStatisticsTotalSchema, + IFormStatisticsTotalModel + >( { formId: { type: Schema.Types.ObjectId, diff --git a/src/app/models/login.server.model.ts b/src/app/models/login.server.model.ts index c0a2b1f8b5..b39a4b812b 100644 --- a/src/app/models/login.server.model.ts +++ b/src/app/models/login.server.model.ts @@ -14,7 +14,7 @@ import { USER_SCHEMA_ID } from './user.server.model' export const LOGIN_SCHEMA_ID = 'Login' -const LoginSchema = new Schema( +const LoginSchema = new Schema( { admin: { type: Schema.Types.ObjectId, @@ -133,7 +133,7 @@ LoginSchema.statics.aggregateLoginStats = function ( } const compileLoginModel = (db: Mongoose) => - db.model(LOGIN_SCHEMA_ID, LoginSchema) as ILoginModel + db.model(LOGIN_SCHEMA_ID, LoginSchema) /** * Retrieves the Login model on the given Mongoose instance. If the model is @@ -143,7 +143,7 @@ const compileLoginModel = (db: Mongoose) => */ const getLoginModel = (db: Mongoose): ILoginModel => { try { - return db.model(LOGIN_SCHEMA_ID) as ILoginModel + return db.model(LOGIN_SCHEMA_ID) } catch { return compileLoginModel(db) } diff --git a/src/app/models/submission.server.model.ts b/src/app/models/submission.server.model.ts index f27c02e9fb..0bf1b1b2d9 100644 --- a/src/app/models/submission.server.model.ts +++ b/src/app/models/submission.server.model.ts @@ -25,7 +25,7 @@ import { FORM_SCHEMA_ID } from './form.server.model' export const SUBMISSION_SCHEMA_ID = 'Submission' -const SubmissionSchema = new Schema( +const SubmissionSchema = new Schema( { form: { type: Schema.Types.ObjectId, @@ -143,7 +143,10 @@ const webhookResponseSchema = new Schema( }, ) -const EncryptSubmissionSchema = new Schema({ +const EncryptSubmissionSchema = new Schema< + IEncryptedSubmissionSchema, + IEncryptSubmissionModel +>({ encryptedContent: { type: String, trim: true, @@ -347,15 +350,15 @@ const compileSubmissionModel = (db: Mongoose): ISubmissionModel => { const Submission = db.model('Submission', SubmissionSchema) Submission.discriminator(SubmissionType.Email, EmailSubmissionSchema) Submission.discriminator(SubmissionType.Encrypt, EncryptSubmissionSchema) - return db.model( + return db.model( SUBMISSION_SCHEMA_ID, SubmissionSchema, - ) as ISubmissionModel + ) } const getSubmissionModel = (db: Mongoose): ISubmissionModel => { try { - return db.model(SUBMISSION_SCHEMA_ID) as ISubmissionModel + return db.model(SUBMISSION_SCHEMA_ID) } catch { return compileSubmissionModel(db) } diff --git a/src/app/models/token.server.model.ts b/src/app/models/token.server.model.ts index f4d2538956..c07699a2b5 100644 --- a/src/app/models/token.server.model.ts +++ b/src/app/models/token.server.model.ts @@ -4,7 +4,7 @@ import { IToken, ITokenModel, ITokenSchema } from '../../types' export const TOKEN_SCHEMA_ID = 'Token' -const TokenSchema = new Schema({ +const TokenSchema = new Schema({ email: { type: String, required: true, diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index f0fbfc20eb..84c334b4e7 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -18,7 +18,7 @@ export const USER_SCHEMA_ID = 'User' const compileUserModel = (db: Mongoose) => { const Agency = getAgencyModel(db) - const UserSchema: Schema = new Schema( + const UserSchema: Schema = new Schema( { email: { type: String, @@ -59,7 +59,8 @@ const compileUserModel = (db: Mongoose) => { if (!phoneNumber) return false return phoneNumber.isValid() }, - message: (props) => `${props.value} is not a valid mobile number`, + message: (props: { value: string }) => + `${props.value} is not a valid mobile number`, }, }, lastAccessed: Date, diff --git a/src/app/modules/bounce/bounce.model.ts b/src/app/modules/bounce/bounce.model.ts index 8f1d9934d0..de7d7f4d47 100644 --- a/src/app/modules/bounce/bounce.model.ts +++ b/src/app/modules/bounce/bounce.model.ts @@ -23,7 +23,7 @@ export interface IBounceModel extends Model { ) => IBounceSchema } -const BounceSchema = new Schema({ +const BounceSchema = new Schema({ formId: { type: Schema.Types.ObjectId, ref: FORM_SCHEMA_ID, diff --git a/src/app/modules/myinfo/myinfo_hash.model.ts b/src/app/modules/myinfo/myinfo_hash.model.ts index bf53a7e406..03f95905f9 100644 --- a/src/app/modules/myinfo/myinfo_hash.model.ts +++ b/src/app/modules/myinfo/myinfo_hash.model.ts @@ -7,7 +7,7 @@ import { FORM_SCHEMA_ID } from '../../models/form.server.model' export const MYINFO_HASH_SCHEMA_ID = 'MyInfoHash' -const MyInfoHashSchema = new Schema( +const MyInfoHashSchema = new Schema( { // We stored a hashed uinFin using a salt stored as a env var // Note: key name not updated to reflect this for backward compatibility purposes diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index 45f7ce5beb..50619491c7 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -227,7 +227,7 @@ export const getFormSubmissionsCount = ( * @returns ok(true) if all emails were sent successfully * @returns err(SendEmailConfirmationError) if any email failed to be sent */ -export const sendEmailConfirmations = ({ +export const sendEmailConfirmations = ({ form, submission, parsedResponses, @@ -235,7 +235,7 @@ export const sendEmailConfirmations = ({ attachments, }: { form: IPopulatedForm - submission: ISubmissionSchema + submission: S parsedResponses: ProcessedFieldResponse[] autoReplyData?: EmailRespondentConfirmationField[] attachments?: IAttachmentInfo[] diff --git a/src/app/modules/verification/verification.controller.ts b/src/app/modules/verification/verification.controller.ts index 6b069e8ab6..a829d760b7 100644 --- a/src/app/modules/verification/verification.controller.ts +++ b/src/app/modules/verification/verification.controller.ts @@ -6,6 +6,7 @@ import { SALT_ROUNDS } from '../../../shared/util/verification' import { PublicTransaction } from '../../../types' import { ErrorDto } from '../../../types/api' import { generateOtpWithHash } from '../../utils/otp' +import { createReqMeta } from '../../utils/request' import { VerificationFactory } from './verification.factory' import { Transaction } from './verification.types' @@ -30,6 +31,7 @@ export const handleCreateTransaction: RequestHandler< const logMeta = { action: 'handleCreateTransaction', formId, + ...createReqMeta(req), } return VerificationFactory.createTransaction(formId) .map((transaction) => { @@ -66,6 +68,7 @@ export const handleGetTransactionMetadata: RequestHandler< const logMeta = { action: 'handleGetTransactionMetadata', transactionId, + ...createReqMeta(req), } return VerificationFactory.getTransactionMetadata(transactionId) .map((publicTransaction) => @@ -99,6 +102,7 @@ export const handleResetField: RequestHandler< action: 'handleResetField', transactionId, fieldId, + ...createReqMeta(req), } return VerificationFactory.resetFieldForTransaction(transactionId, fieldId) .map(() => res.sendStatus(StatusCodes.OK)) @@ -130,6 +134,7 @@ export const handleGetOtp: RequestHandler< action: 'handleGetOtp', transactionId, fieldId, + ...createReqMeta(req), } return generateOtpWithHash(logMeta, SALT_ROUNDS) .andThen(({ otp, hashedOtp }) => @@ -171,6 +176,7 @@ export const handleVerifyOtp: RequestHandler< action: 'handleVerifyOtp', transactionId, fieldId, + ...createReqMeta(req), } return VerificationFactory.verifyOtp(transactionId, fieldId, otp) .map((signedData) => res.status(StatusCodes.OK).json(signedData)) diff --git a/src/app/modules/verification/verification.model.ts b/src/app/modules/verification/verification.model.ts index e1be83a247..53d96c3d82 100644 --- a/src/app/modules/verification/verification.model.ts +++ b/src/app/modules/verification/verification.model.ts @@ -33,7 +33,10 @@ const VerificationFieldSchema = new Schema({ }) const compileVerificationModel = (db: Mongoose): IVerificationModel => { - const VerificationSchema = new Schema({ + const VerificationSchema = new Schema< + IVerificationSchema, + IVerificationModel + >({ formId: { type: Schema.Types.ObjectId, ref: FORM_SCHEMA_ID, diff --git a/src/app/routes/api/v3/auth/__tests__/auth.routes.spec.ts b/src/app/routes/api/v3/auth/__tests__/auth.routes.spec.ts new file mode 100644 index 0000000000..d69a1d18f7 --- /dev/null +++ b/src/app/routes/api/v3/auth/__tests__/auth.routes.spec.ts @@ -0,0 +1,564 @@ +import { pick } from 'lodash' +import { errAsync, okAsync } from 'neverthrow' +import supertest, { Session } from 'supertest-session' +import validator from 'validator' + +import MailService from 'src/app/services/mail/mail.service' +import { HashingError } from 'src/app/utils/hash' +import * as OtpUtils from 'src/app/utils/otp' +import { IAgencySchema } from 'src/types' + +import { setupApp } from 'tests/integration/helpers/express-setup' +import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' +import dbHandler from 'tests/unit/backend/helpers/jest-db' + +import * as AuthService from '../../../../../modules/auth/auth.service' +import { DatabaseError } from '../../../../../modules/core/core.errors' +import * as UserService from '../../../../../modules/user/user.service' +import { MailSendError } from '../../../../../services/mail/mail.errors' +import { AuthRouter } from '../auth.routes' + +const app = setupApp('/auth', AuthRouter) + +describe('auth.routes', () => { + let request: Session + + beforeAll(async () => await dbHandler.connect()) + beforeEach(() => { + request = supertest(app) + }) + afterEach(async () => { + await dbHandler.clearDatabase() + jest.restoreAllMocks() + }) + afterAll(async () => await dbHandler.closeDatabase()) + + describe('POST /auth/email/validate', () => { + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/email/validate') + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ body: { key: 'email' } }), + ) + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/email/validate') + .send({ email: invalidEmail }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { key: 'email', message: 'Please enter a valid email' }, + }), + ) + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = 'test@example.com' + + // Act + const response = await request + .post('/auth/email/validate') + .send({ email: validEmailWithInvalidDomain }) + + // Assert + expect(response.status).toEqual(401) + expect(response.body).toEqual( + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) + }) + + it('should return 200 when domain of body.email exists in Agency collection', async () => { + // Arrange + // Insert agency + const validDomain = 'example.com' + const validEmail = `test@${validDomain}` + await dbHandler.insertAgency({ mailDomain: validDomain }) + + // Act + const response = await request + .post('/auth/email/validate') + .send({ email: validEmail }) + + // Assert + expect(response.status).toEqual(200) + expect(response.text).toEqual('OK') + }) + + it('should return 500 when validating domain returns a database error', async () => { + // Arrange + // Insert agency + const validDomain = 'example.com' + const validEmail = `test@${validDomain}` + const mockErrorString = 'Unable to validate email domain.' + await dbHandler.insertAgency({ mailDomain: validDomain }) + + const getAgencySpy = jest + .spyOn(AuthService, 'validateEmailDomain') + .mockReturnValueOnce(errAsync(new DatabaseError(mockErrorString))) + + // Act + const response = await request + .post('/auth/email/validate') + .send({ email: validEmail }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual(mockErrorString) + }) + }) + + describe('POST /auth/otp/generate', () => { + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + const INVALID_DOMAIN = 'example.org' + + beforeEach(async () => dbHandler.insertAgency({ mailDomain: VALID_DOMAIN })) + + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/otp/generate') + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ body: { key: 'email' } }), + ) + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: invalidEmail }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { key: 'email', message: 'Please enter a valid email' }, + }), + ) + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = `test@${INVALID_DOMAIN}` + expect(validator.isEmail(validEmailWithInvalidDomain)).toEqual(true) + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: validEmailWithInvalidDomain }) + + // Assert + expect(response.status).toEqual(401) + expect(response.body).toEqual({ + message: + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + }) + }) + + it('should return 500 when error occurs whilst creating OTP', async () => { + // Arrange + const createLoginOtpSpy = jest + .spyOn(AuthService, 'createLoginOtp') + .mockReturnValueOnce(errAsync(new HashingError())) + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: VALID_EMAIL }) + + // Assert + expect(createLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual({ + message: + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + }) + }) + + it('should return 500 when error occurs whilst sending login OTP', async () => { + // Arrange + const sendLoginOtpSpy = jest + .spyOn(MailService, 'sendLoginOtp') + .mockReturnValueOnce(errAsync(new MailSendError('some error'))) + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: VALID_EMAIL }) + + // Assert + expect(sendLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual({ + message: + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + }) + }) + + it('should return 500 when validating domain returns a database error', async () => { + // Arrange + const getAgencySpy = jest + .spyOn(AuthService, 'validateEmailDomain') + .mockReturnValueOnce(errAsync(new DatabaseError())) + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: VALID_EMAIL }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual({ + message: + 'Failed to send login OTP. Please try again later and if the problem persists, contact us.', + }) + }) + + it('should return 200 when otp is sent successfully', async () => { + // Arrange + const sendLoginOtpSpy = jest + .spyOn(MailService, 'sendLoginOtp') + .mockReturnValueOnce(okAsync(true)) + + // Act + const response = await request + .post('/auth/otp/generate') + .send({ email: VALID_EMAIL }) + + // Assert + expect(sendLoginOtpSpy).toHaveBeenCalled() + expect(response.status).toEqual(200) + expect(response.body).toEqual(`OTP sent to ${VALID_EMAIL}`) + }) + }) + + describe('POST /auth/otp/verify', () => { + const MOCK_VALID_OTP = '123456' + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + const INVALID_DOMAIN = 'example.org' + + let defaultAgency: IAgencySchema + + beforeEach(async () => { + defaultAgency = await dbHandler.insertAgency({ + mailDomain: VALID_DOMAIN, + }) + jest.spyOn(OtpUtils, 'generateOtp').mockReturnValue(MOCK_VALID_OTP) + }) + + it('should return 400 when body.email is not provided as a param', async () => { + // Act + const response = await request.post('/auth/otp/verify').send({ + otp: MOCK_VALID_OTP, + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ body: { key: 'email' } }), + ) + }) + + it('should return 400 when body.otp is not provided as a param', async () => { + // Act + const response = await request.post('/auth/otp/verify').send({ + email: VALID_EMAIL, + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ body: { key: 'otp' } }), + ) + }) + + it('should return 400 when body.email is invalid', async () => { + // Arrange + const invalidEmail = 'not an email' + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: invalidEmail, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { key: 'email', message: 'Please enter a valid email' }, + }), + ) + }) + + it('should return 400 when body.otp is less than 6 digits', async () => { + // Act + const response = await request.post('/auth/otp/verify').send({ + email: VALID_EMAIL, + otp: '12345', + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { key: 'otp', message: 'Please enter a valid otp' }, + }), + ) + }) + + it('should return 400 when body.otp is 6 characters but does not consist purely of digits', async () => { + // Act + const response = await request.post('/auth/otp/verify').send({ + email: VALID_EMAIL, + otp: '123abc', + }) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { key: 'otp', message: 'Please enter a valid otp' }, + }), + ) + }) + + it('should return 401 when domain of body.email does not exist in Agency collection', async () => { + // Arrange + const validEmailWithInvalidDomain = `test@${INVALID_DOMAIN}` + expect(validator.isEmail(validEmailWithInvalidDomain)).toEqual(true) + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: validEmailWithInvalidDomain, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(401) + expect(response.body).toEqual( + 'This is not a whitelisted public service email domain. Please log in with your official government or government-linked email address.', + ) + }) + + it('should return 500 when validating domain returns a database error', async () => { + // Arrange + const getAgencySpy = jest + .spyOn(AuthService, 'validateEmailDomain') + .mockReturnValueOnce(errAsync(new DatabaseError())) + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + expect(getAgencySpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual('Something went wrong. Please try again.') + }) + + it('should return 422 when hash does not exist for body.otp', async () => { + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual( + expect.stringContaining( + 'OTP has expired. Please request for a new OTP.', + ), + ) + }) + + it('should return 422 when body.otp is invalid', async () => { + // Arrange + const invalidOtp = '654321' + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: invalidOtp }) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual('OTP is invalid. Please try again.') + }) + + it('should return 422 when invalid body.otp has been attempted too many times', async () => { + // Arrange + const invalidOtp = '654321' + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + // Attempt invalid OTP for MAX_OTP_ATTEMPTS. + const verifyPromises = [] + for (let i = 0; i < AuthService.MAX_OTP_ATTEMPTS; i++) { + verifyPromises.push( + request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: invalidOtp }), + ) + } + const results = (await Promise.all(verifyPromises)).map((resolve) => + pick(resolve, ['status', 'body']), + ) + // Should be all invalid OTP responses. + expect(results).toEqual( + Array(AuthService.MAX_OTP_ATTEMPTS).fill({ + status: 422, + body: 'OTP is invalid. Please try again.', + }), + ) + + // Act again, this time with a valid OTP. + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + // Should still reject with max OTP attempts error. + expect(response.status).toEqual(422) + expect(response.body).toEqual( + 'You have hit the max number of attempts. Please request for a new OTP.', + ) + }) + + it('should return 200 with user object when body.otp is a valid OTP', async () => { + // Arrange + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + expect(response.status).toEqual(200) + // Body should be an user object. + expect(response.body).toMatchObject({ + // Required since that's how the data is sent out from the application. + agency: JSON.parse(JSON.stringify(defaultAgency.toObject())), + _id: expect.any(String), + created: expect.any(String), + email: VALID_EMAIL, + }) + // Should have session cookie returned. + const sessionCookie = request.cookies.find( + (cookie) => cookie.name === 'connect.sid', + ) + expect(sessionCookie).toBeDefined() + }) + + it('should return 500 when upserting user document fails', async () => { + // Arrange + // Request for OTP so the hash exists. + await requestForOtp(VALID_EMAIL) + + // Mock error returned when creating user + const upsertSpy = jest + .spyOn(UserService, 'retrieveUser') + .mockReturnValueOnce(errAsync(new DatabaseError('some error'))) + + // Act + const response = await request + .post('/auth/otp/verify') + .send({ email: VALID_EMAIL, otp: MOCK_VALID_OTP }) + + // Assert + // Should have reached this spy. + expect(upsertSpy).toBeCalled() + expect(response.status).toEqual(500) + expect(response.body).toEqual( + expect.stringContaining('Failed to process OTP.'), + ) + }) + }) + + describe('GET /auth/logout', () => { + const MOCK_VALID_OTP = '123456' + const VALID_DOMAIN = 'example.com' + const VALID_EMAIL = `test@${VALID_DOMAIN}` + + beforeEach(async () => { + await dbHandler.insertAgency({ + mailDomain: VALID_DOMAIN, + }) + jest.spyOn(OtpUtils, 'generateOtp').mockReturnValue(MOCK_VALID_OTP) + }) + + it('should return 200 and clear cookies when signout is successful', async () => { + // Act + // Sign in user + await signInUser(VALID_EMAIL, MOCK_VALID_OTP) + + // Arrange + const response = await request.get('/auth/logout') + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toEqual({ message: 'Sign out successful' }) + // connect.sid should now be empty. + expect(response.header['set-cookie'][0]).toEqual( + expect.stringContaining('connect.sid=;'), + ) + }) + + it('should return 200 even when user has not signed in before', async () => { + // Note that no log in calls have been made with request yet. + // Act + const response = await request.get('/auth/logout') + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toEqual({ message: 'Sign out successful' }) + }) + }) + + // Helper functions + const requestForOtp = async (email: string) => { + // Set that so no real mail is sent. + jest.spyOn(MailService, 'sendLoginOtp').mockReturnValue(okAsync(true)) + + const response = await request.post('/auth/otp/generate').send({ email }) + expect(response.body).toEqual(`OTP sent to ${email}`) + } + + const signInUser = async (email: string, otp: string) => { + await requestForOtp(email) + const response = await request.post('/auth/otp/verify').send({ email, otp }) + + // Assert + // Should have session cookie returned. + const sessionCookie = request.cookies.find( + (cookie) => cookie.name === 'connect.sid', + ) + expect(sessionCookie).toBeDefined() + return response.body + } +}) diff --git a/src/app/routes/api/v3/auth/auth.routes.ts b/src/app/routes/api/v3/auth/auth.routes.ts new file mode 100644 index 0000000000..b931d9d6fb --- /dev/null +++ b/src/app/routes/api/v3/auth/auth.routes.ts @@ -0,0 +1,95 @@ +import { celebrate, Joi, Segments } from 'celebrate' +import { Router } from 'express' + +import { rateLimitConfig } from '../../../../../config/config' +import * as AuthController from '../../../../modules/auth/auth.controller' +import { limitRate } from '../../../../utils/limit-rate' + +export const AuthRouter = Router() +/** + * Check if email domain is a valid agency + * @route POST /auth/email/validate + * @group admin + * @param body.email the user's email to validate domain for + * @return 200 when email domain is valid + * @return 401 when email domain is invalid + */ +AuthRouter.post( + '/email/validate', + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + }), + }), + AuthController.handleCheckUser, +) + +/** + * Send a one-time password (OTP) to the specified email address + * as part of the login procedure. + * @route POST /auth/otp/generate + * @group admin + * @param body.email the user's email to validate domain for + * @produces application/json + * @consumes application/json + * @return 200 when OTP has been been successfully sent + * @return 401 when email domain is invalid + * @return 500 when FormSG was unable to generate the OTP, or create/send the email that delivers the OTP to the user's email address + */ +AuthRouter.post( + '/otp/generate', + limitRate({ max: rateLimitConfig.sendAuthOtp }), + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + }), + }), + AuthController.handleLoginSendOtp, +) + +/** + * Verify the one-time password (OTP) for the specified email address + * as part of the login procedure. + * @route POST /auth/otp/verify + * @group admin + * @param body.email the user's email + * @param body.otp the otp to verify + * @headers 200.set-cookie contains the session cookie upon login + * @returns 200 when user has successfully logged in, with session cookie set + * @returns 401 when the email domain is invalid + * @returns 422 when the OTP is invalid + * @returns 500 when error occurred whilst verifying the OTP + */ +AuthRouter.post( + '/otp/verify', + celebrate({ + [Segments.BODY]: Joi.object().keys({ + email: Joi.string() + .required() + .email() + .message('Please enter a valid email'), + otp: Joi.string() + .required() + .regex(/^\d{6}$/) + .message('Please enter a valid otp'), + }), + }), + AuthController.handleLoginVerifyOtp, +) + +/** + * Sign the user out of the session by clearing the relevant session cookie + * @route GET /auth/logout + * @group admin + * @headers 200.clear-cookie clears cookie upon signout + * @returns 200 when user has signed out successfully + * @returns 400 when the request does not contain a session + * @returns 500 when the session fails to be destroyed + */ +AuthRouter.get('/logout', AuthController.handleSignout) diff --git a/src/app/routes/api/v3/auth/index.ts b/src/app/routes/api/v3/auth/index.ts new file mode 100644 index 0000000000..b035f15671 --- /dev/null +++ b/src/app/routes/api/v3/auth/index.ts @@ -0,0 +1 @@ +export { AuthRouter } from './auth.routes' diff --git a/src/app/routes/api/v3/v3.routes.ts b/src/app/routes/api/v3/v3.routes.ts index 8bd36bdd9e..e4a2061fe9 100644 --- a/src/app/routes/api/v3/v3.routes.ts +++ b/src/app/routes/api/v3/v3.routes.ts @@ -1,7 +1,9 @@ import { Router } from 'express' import { AdminRouter } from './admin' +import { AuthRouter } from './auth' export const V3Router = Router() V3Router.use('/admin', AdminRouter) +V3Router.use('/auth', AuthRouter) diff --git a/src/app/services/sms/sms_count.server.model.ts b/src/app/services/sms/sms_count.server.model.ts index ea27658a2f..e7638e2ac8 100644 --- a/src/app/services/sms/sms_count.server.model.ts +++ b/src/app/services/sms/sms_count.server.model.ts @@ -83,7 +83,7 @@ const BouncedSubmissionSmsCountSchema = new Schema { - const SmsCountSchema = new Schema( + const SmsCountSchema = new Schema( { msgSrvcSid: { type: String, diff --git a/src/public/modules/users/services/auth.client.service.js b/src/public/modules/users/services/auth.client.service.js index 9be1957080..78e7703c46 100644 --- a/src/public/modules/users/services/auth.client.service.js +++ b/src/public/modules/users/services/auth.client.service.js @@ -69,7 +69,7 @@ function Auth($q, $http, $state, $window) { function checkUser(credentials) { let deferred = $q.defer() - $http.post('/auth/checkuser', credentials).then( + $http.post('/api/v3/auth/email/validate', credentials).then( function (response) { deferred.resolve(response.data) }, @@ -82,7 +82,7 @@ function Auth($q, $http, $state, $window) { function sendOtp(credentials) { let deferred = $q.defer() - $http.post('/auth/sendotp', credentials).then( + $http.post('/api/v3/auth/otp/generate', credentials).then( function (response) { deferred.resolve(response.data) }, @@ -95,7 +95,7 @@ function Auth($q, $http, $state, $window) { function verifyOtp(credentials) { let deferred = $q.defer() - $http.post('/auth/verifyotp', credentials).then( + $http.post('/api/v3/auth/otp/verify', credentials).then( function (response) { setUser(response.data) deferred.resolve() @@ -108,7 +108,7 @@ function Auth($q, $http, $state, $window) { } function signOut() { - $http.get('/auth/signout').then( + $http.get('/api/v3/auth/logout').then( function () { $window.localStorage.removeItem('user') // Clear contact banner on logout diff --git a/src/types/vendor/mongoose.d.ts b/src/types/vendor/mongoose.d.ts new file mode 100644 index 0000000000..917356050f --- /dev/null +++ b/src/types/vendor/mongoose.d.ts @@ -0,0 +1,33 @@ +/** + * Additional type declarations for mongoose to fit our use case, + * to accommodate the non-standard but compatible use of types + * in schema registration + */ +declare module 'mongoose' { + namespace Schema { + namespace Types { + /** + * A DocumentArray with a discriminator function that takes in a + * type-generic Schema. + */ + class DocumentArrayWithLooseDiscriminator extends DocumentArray { + /** + * In the built-in type declarations in + * version 5.12 of mongoose, discriminator() expects a Schema + * (and hence Schema). This does not work; a Schema + * for a subtype of Document is not a Schema for a Document, as + * Schema.methods expect to operate on the Schema's document type, + * which is not necessarily a Document. + * + * Address this by overriding the definition and provide a type-generic + * Schema argument. + */ + discriminator( + name: string, + schema: Schema, + tag?: string, + ): unknown + } + } + } +}