diff --git a/.eslintrc b/.eslintrc index bd6442f48d..cba138717a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,7 +24,7 @@ }, "project": "./tsconfig.json" }, - "plugins": ["@typescript-eslint", "import", "simple-import-sort"], + "plugins": ["@typescript-eslint", "import", "simple-import-sort", "typesafe"], "extends": ["plugin:@typescript-eslint/recommended"], "rules": { // Rules for auto sort of imports @@ -54,7 +54,8 @@ "import/newline-after-import": "error", "import/no-duplicates": "error", "@typescript-eslint/no-floating-promises": 2, - "@typescript-eslint/no-unused-vars": 2 + "@typescript-eslint/no-unused-vars": 2, + "typesafe/no-throw-sync-func": "error" } }, { "files": ["*.spec.ts"], "extends": ["plugin:jest/recommended"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d13353bd8..669357c2b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,48 @@ 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). +#### [v4.52.0](https://github.com/opengovsg/FormSG/compare/v4.51.0...v4.52.0) + +- fix(deps): bump @opengovsg/ng-file-upload from 12.2.14 to 12.2.15 [`#965`](https://github.com/opengovsg/FormSG/pull/965) +- chore(deps-dev): bump eslint-plugin-simple-import-sort [`#966`](https://github.com/opengovsg/FormSG/pull/966) +- fix(AuthClientCtl): cancel timeout promise on page change [`#971`](https://github.com/opengovsg/FormSG/pull/971) +- Show OTP delay notification [`#748`](https://github.com/opengovsg/FormSG/pull/748) +- Bulk Attachment Download Frontend [`#640`](https://github.com/opengovsg/FormSG/pull/640) +- fix: upgrade neverthrow from 3.0.0 to 3.1.2 [`#968`](https://github.com/opengovsg/FormSG/pull/968) +- feat/type-safe: Encourage type-safe coding practices with eslint-plugin-typesafe [`#943`](https://github.com/opengovsg/FormSG/pull/943) +- feat: send SMS notification when submissions bounce [`#961`](https://github.com/opengovsg/FormSG/pull/961) +- chore(deps-dev): bump @opengovsg/mockpass from 2.6.0 to 2.6.1 [`#960`](https://github.com/opengovsg/FormSG/pull/960) +- chore(deps-dev): bump sinon from 9.2.1 to 9.2.2 [`#959`](https://github.com/opengovsg/FormSG/pull/959) +- fix(deps): bump mongoose from 5.11.8 to 5.11.9 [`#958`](https://github.com/opengovsg/FormSG/pull/958) +- chore(deps-dev): bump ts-node-dev from 1.0.0 to 1.1.1 [`#957`](https://github.com/opengovsg/FormSG/pull/957) +- fix(deps): bump aws-sdk from 2.817.0 to 2.818.0 [`#956`](https://github.com/opengovsg/FormSG/pull/956) +- chore(deps-dev): bump eslint from 7.14.0 to 7.16.0 [`#955`](https://github.com/opengovsg/FormSG/pull/955) +- fix(deps): bump @sentry/browser from 5.29.1 to 5.29.2 [`#951`](https://github.com/opengovsg/FormSG/pull/951) +- chore(deps-dev): bump @typescript-eslint/eslint-plugin from 4.0.1 to 4.11.0 [`#927`](https://github.com/opengovsg/FormSG/pull/927) +- chore(deps-dev): bump @babel/plugin-transform-runtime [`#952`](https://github.com/opengovsg/FormSG/pull/952) +- fix(deps): bump aws-sdk from 2.805.0 to 2.817.0 [`#953`](https://github.com/opengovsg/FormSG/pull/953) +- fix(deps): bump nodemailer from 6.4.16 to 6.4.17 [`#954`](https://github.com/opengovsg/FormSG/pull/954) +- test: add Joi validation integration tests [`#933`](https://github.com/opengovsg/FormSG/pull/933) +- fix(deps): bump @sentry/integrations from 5.29.0 to 5.29.2 [`#946`](https://github.com/opengovsg/FormSG/pull/946) +- fix(deps): bump winston-cloudwatch from 2.4.0 to 2.5.0 [`#945`](https://github.com/opengovsg/FormSG/pull/945) +- refactor: migrate submissions controller to TypeScript [`#881`](https://github.com/opengovsg/FormSG/pull/881) +- chore(deps-dev): bump @typescript-eslint/parser from 4.10.0 to 4.11.0 [`#944`](https://github.com/opengovsg/FormSG/pull/944) +- fix(deps): bump axios from 0.21.0 to 0.21.1 [`#947`](https://github.com/opengovsg/FormSG/pull/947) +- chore(deps-dev): bump testcafe from 1.9.4 to 1.10.1 [`#942`](https://github.com/opengovsg/FormSG/pull/942) +- chore(deps-dev): bump ngrok from 3.3.0 to 3.4.0 [`#939`](https://github.com/opengovsg/FormSG/pull/939) +- chore(deps-dev): bump ts-node from 9.0.0 to 9.1.1 [`#937`](https://github.com/opengovsg/FormSG/pull/937) +- fix(deps): bump angular-ui-router from 1.0.28 to 1.0.29 [`#936`](https://github.com/opengovsg/FormSG/pull/936) +- build: merge Release 4.51.0 into develop [`#932`](https://github.com/opengovsg/FormSG/pull/932) +- test: add SPCP authentication integration tests [`#921`](https://github.com/opengovsg/FormSG/pull/921) +- fix(deps): bump boxicons from 1.8.0 to 1.8.1 [`#926`](https://github.com/opengovsg/FormSG/pull/926) +- fix(deps): bump @opengovsg/myinfo-gov-client from 2.1.2 to 2.1.3 [`#924`](https://github.com/opengovsg/FormSG/pull/924) +- chore(deps-dev): bump eslint-plugin-prettier from 3.1.4 to 3.3.0 [`#923`](https://github.com/opengovsg/FormSG/pull/923) +- chore(deps-dev): bump @babel/preset-env from 7.12.7 to 7.12.11 [`#925`](https://github.com/opengovsg/FormSG/pull/925) + #### [v4.51.0](https://github.com/opengovsg/FormSG/compare/v4.50.3...v4.51.0) +> 22 December 2020 + - refactor: prepareEncryptSubmission to typescript [`#891`](https://github.com/opengovsg/FormSG/pull/891) - build: merge 4.50.3 into develop [`#919`](https://github.com/opengovsg/FormSG/pull/919) - fix: backend validation does not prevent responses on hidden fields [`#809`](https://github.com/opengovsg/FormSG/pull/809) @@ -44,6 +84,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: upgrade twilio from 3.51.0 to 3.52.0 [`#869`](https://github.com/opengovsg/FormSG/pull/869) - chore(deps-dev): bump husky from 4.3.5 to 4.3.6 [`#877`](https://github.com/opengovsg/FormSG/pull/877) - chore(deps-dev): bump @babel/core from 7.12.3 to 7.12.10 [`#878`](https://github.com/opengovsg/FormSG/pull/878) +- chore: bump version to 4.51.0 [`f2facdb`](https://github.com/opengovsg/FormSG/commit/f2facdb7bf18472896495fec2090a0e368ff7d56) - chore: bump version to 4.50.3 [`4b0ecec`](https://github.com/opengovsg/FormSG/commit/4b0ecec413c2176adaf5e82065486444bddc1c2f) #### [v4.50.3](https://github.com/opengovsg/FormSG/compare/v4.50.2...v4.50.3) diff --git a/package-lock.json b/package-lock.json index 2d055e728e..b096ee5e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "4.51.0", + "version": "4.52.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -205,18 +205,18 @@ }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -236,28 +236,28 @@ }, "dependencies": { "browserslist": { - "version": "4.14.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", - "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz", + "integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001157", + "caniuse-lite": "^1.0.30001165", "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.591", + "electron-to-chromium": "^1.3.621", "escalade": "^3.1.1", - "node-releases": "^1.1.66" + "node-releases": "^1.1.67" } }, "caniuse-lite": { - "version": "1.0.30001161", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001161.tgz", - "integrity": "sha512-JharrCDxOqPLBULF9/SPa6yMcBRTjZARJ6sc3cuKrPfyIk64JN6kuMINWqA99Xc8uElMFcROliwtz0n9pYej+g==", + "version": "1.0.30001170", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz", + "integrity": "sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==", "dev": true }, "electron-to-chromium": { - "version": "1.3.607", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.607.tgz", - "integrity": "sha512-h2SYNaBnlplGS0YyXl8oJWokfcNxVjJANQfMCsQefG6OSuAuNIeW+A8yGT/ci+xRoBb3k2zq1FrOvkgoKBol8g==", + "version": "1.3.631", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.631.tgz", + "integrity": "sha512-mPEG/52142po0XK1jQkZtbMmp38MZtQ3JDFItYxV65WXyhxDYEQ54tP4rb93m0RbMlZqQ+4zBw2N7UumSgGfbA==", "dev": true }, "node-releases": { @@ -282,43 +282,43 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.12.5", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-member-expression-to-functions": { @@ -331,39 +331,39 @@ } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", - "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.12.7" + "@babel/types": "^7.12.10" } }, "@babel/helper-replace-supers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", - "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/highlight": { @@ -378,9 +378,9 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/template": { @@ -395,29 +395,29 @@ } }, "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", + "@babel/generator": "^7.12.10", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -444,27 +444,27 @@ }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -492,18 +492,18 @@ }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -540,18 +540,18 @@ }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -791,67 +791,67 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.12.5", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/helper-wrap-function": { @@ -878,9 +878,9 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/template": { @@ -895,29 +895,29 @@ } }, "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", + "@babel/generator": "^7.12.10", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -983,18 +983,18 @@ }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -1017,9 +1017,9 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", - "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", + "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", "dev": true }, "@babel/helper-wrap-function": { @@ -1221,6 +1221,25 @@ } } }, + "@babel/plugin-proposal-decorators": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.12.tgz", + "integrity": "sha512-fhkE9lJYpw2mjHelBpM2zCbaA11aov2GJs7q4cFaXNrWx0H3bW58H9Esy2rdtYOghFBEYUDRIpvlgi+ZD+AvvQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-decorators": "^7.12.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", @@ -1467,6 +1486,23 @@ } } }, + "@babel/plugin-syntax-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", + "integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -1485,6 +1521,23 @@ "@babel/helper-plugin-utils": "^7.8.3" } }, + "@babel/plugin-syntax-flow": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz", + "integrity": "sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-syntax-function-sent": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.10.1.tgz", @@ -1520,6 +1573,23 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -1765,6 +1835,24 @@ } } }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.12.10.tgz", + "integrity": "sha512-0ti12wLTLeUIzu9U7kjqIn4MyOL7+Wibc7avsHhj4o1l5C0ATs8p2IMHrVYjm9t9wzhfEO6S3kxax0Rpdo8LTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-flow": "^7.12.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/plugin-transform-for-of": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz", @@ -1785,32 +1873,32 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { @@ -1820,9 +1908,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/highlight": { @@ -1837,9 +1925,9 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/template": { @@ -1854,12 +1942,12 @@ } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -1959,9 +2047,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true } } @@ -2021,43 +2109,43 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.12.5", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-member-expression-to-functions": { @@ -2070,12 +2158,12 @@ } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", - "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.12.7" + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { @@ -2085,30 +2173,30 @@ "dev": true }, "@babel/helper-replace-supers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", - "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/highlight": { @@ -2123,9 +2211,9 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/template": { @@ -2140,29 +2228,29 @@ } }, "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", + "@babel/generator": "^7.12.10", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -2212,6 +2300,123 @@ } } }, + "@babel/plugin-transform-react-display-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", + "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz", + "integrity": "sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.10", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1", + "@babel/types": "^7.12.12" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz", + "integrity": "sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.12.12" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/plugin-transform-regenerator": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", @@ -2239,14 +2444,13 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz", - "integrity": "sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.10.tgz", + "integrity": "sha512-xOrUfzPxw7+WDm9igMgQCbO3cJKymX7dFdsgRr1eu9n3KjjyU4pptIXbXPseQDquw+W+RuJEJMHKHNsPNNm3CA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-module-imports": "^7.12.5", "@babel/helper-plugin-utils": "^7.10.4", - "resolve": "^1.8.1", "semver": "^5.5.1" }, "dependencies": { @@ -2328,9 +2532,9 @@ } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", - "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", + "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" @@ -2380,16 +2584,16 @@ } }, "@babel/preset-env": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz", - "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz", + "integrity": "sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==", "dev": true, "requires": { "@babel/compat-data": "^7.12.7", "@babel/helper-compilation-targets": "^7.12.5", "@babel/helper-module-imports": "^7.12.5", "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", + "@babel/helper-validator-option": "^7.12.11", "@babel/plugin-proposal-async-generator-functions": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-dynamic-import": "^7.12.1", @@ -2418,7 +2622,7 @@ "@babel/plugin-transform-arrow-functions": "^7.12.1", "@babel/plugin-transform-async-to-generator": "^7.12.1", "@babel/plugin-transform-block-scoped-functions": "^7.12.1", - "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.11", "@babel/plugin-transform-classes": "^7.12.1", "@babel/plugin-transform-computed-properties": "^7.12.1", "@babel/plugin-transform-destructuring": "^7.12.1", @@ -2444,42 +2648,42 @@ "@babel/plugin-transform-spread": "^7.12.1", "@babel/plugin-transform-sticky-regex": "^7.12.7", "@babel/plugin-transform-template-literals": "^7.12.1", - "@babel/plugin-transform-typeof-symbol": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.10", "@babel/plugin-transform-unicode-escapes": "^7.12.1", "@babel/plugin-transform-unicode-regex": "^7.12.1", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.12.7", - "core-js-compat": "^3.7.0", + "@babel/types": "^7.12.11", + "core-js-compat": "^3.8.0", "semver": "^5.5.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.12.5", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-define-map": { @@ -2494,23 +2698,23 @@ } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.10" } }, "@babel/helper-member-expression-to-functions": { @@ -2523,12 +2727,12 @@ } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", - "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.12.7" + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { @@ -2538,30 +2742,30 @@ "dev": true }, "@babel/helper-replace-supers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", - "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", "dev": true }, "@babel/highlight": { @@ -2576,29 +2780,11 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", - "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", - "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, "@babel/plugin-transform-arrow-functions": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", @@ -2609,9 +2795,9 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", - "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.11.tgz", + "integrity": "sha512-atR1Rxc3hM+VPg/NvNvfYw0npQEAcHuJ+MGZnFn6h3bo+1U3BWXMdFMlvVRApBTWKQMX7SOwRJZA5FBF/JQbvA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" @@ -2654,29 +2840,29 @@ } }, "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", + "@babel/generator": "^7.12.10", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -2692,6 +2878,24 @@ } } }, + "@babel/preset-flow": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.12.1.tgz", + "integrity": "sha512-UAoyMdioAhM6H99qPoKvpHMzxmNVXno8GYU/7vZmGaHk6/KqfDYL1W0NxszVbJ2EP271b7e6Ox+Vk2A9QsB3Sw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-flow-strip-types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/preset-modules": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", @@ -2705,6 +2909,27 @@ "esutils": "^2.0.2" } }, + "@babel/preset-react": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz", + "integrity": "sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.10", + "@babel/plugin-transform-react-jsx-development": "^7.12.7", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, "@babel/runtime": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", @@ -2790,9 +3015,9 @@ } }, "@eslint/eslintrc": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", - "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -3501,9 +3726,9 @@ } }, "@opengovsg/mockpass": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opengovsg/mockpass/-/mockpass-2.6.0.tgz", - "integrity": "sha512-fFS0PgyfS/FvaEN83MaDS1+5EUxyEBnDG4i34mz8wggPjs3lMbB4+zGYBQWj74o+Uobm4AX5n4W2piUjXYeqXA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opengovsg/mockpass/-/mockpass-2.6.1.tgz", + "integrity": "sha512-oSXXNuii0t03l87XoD4Yh8XxEHtAxJNERDmk6hVilpy80Fxx56j/UPFdxTSxvBE+mvJwVSumYc4+ERx7GJfx9A==", "dev": true, "requires": { "base-64": "^1.0.0", @@ -3533,16 +3758,16 @@ } }, "@opengovsg/myinfo-gov-client": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@opengovsg/myinfo-gov-client/-/myinfo-gov-client-2.1.2.tgz", - "integrity": "sha512-YrHswABeH1Gcl3ga6onQOMa09Ohqe57a+lrCOdFcehlvNSc6QOmN8Sd8Z/nkFJr2xOsgPGl+kcRTYfK4vooV9g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@opengovsg/myinfo-gov-client/-/myinfo-gov-client-2.1.3.tgz", + "integrity": "sha512-zJui6FraxOq8/A8QB5bmO3sRvNL5ZK6iEvYDv6eJPYDCJKQc51JI0tnZuzkirolD++C2qpqC9S23QZwR+bBG4Q==", "requires": { "axios": "^0.21.0", "jose": "^0.3.2", "node-jose": "^2.0.0", "path": "^0.12.7", "qs": "^6.9.4", - "request": "^2.88.0" + "request": "^2.88.2" }, "dependencies": { "qs": { @@ -3553,9 +3778,9 @@ } }, "@opengovsg/ng-file-upload": { - "version": "12.2.14", - "resolved": "https://registry.npmjs.org/@opengovsg/ng-file-upload/-/ng-file-upload-12.2.14.tgz", - "integrity": "sha512-YZx96jBfRCgIbGErhYLfnsdZ199laYJt6zknI/ea17tPhWw4+kYE09a6G5dckiN/QCBxHeZRsjLU6WRRKHleKw==" + "version": "12.2.15", + "resolved": "https://registry.npmjs.org/@opengovsg/ng-file-upload/-/ng-file-upload-12.2.15.tgz", + "integrity": "sha512-YqR6GIsum9K7Cg6wOTxwJnKP+KDOxbZ9dnQE2/M47vP0ynXyTadvwflGBukzJ/MhzrS2R6buNhFjFnVJRXJinw==" }, "@opengovsg/spcp-auth-client": { "version": "1.4.0", @@ -3581,125 +3806,70 @@ } }, "@sentry/browser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.1.tgz", - "integrity": "sha512-cVlXoQBJ64eNNkQuOB+bS6sK5KWV+Fw+ZYxT+XqjpeXkOPaxh8aeoi9CHz2DsFfbLV91P4AnXZEUdDl+7ktQNg==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.2.tgz", + "integrity": "sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ==", "requires": { - "@sentry/core": "5.29.1", - "@sentry/types": "5.29.1", - "@sentry/utils": "5.29.1", + "@sentry/core": "5.29.2", + "@sentry/types": "5.29.2", + "@sentry/utils": "5.29.2", "tslib": "^1.9.3" - }, - "dependencies": { - "@sentry/types": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.1.tgz", - "integrity": "sha512-QXZBA1gJheMYTGFV+UUhr3+jKpGZqPx8kEJABs8htlKabCDJlEeoFNmeqPuVxCxukoy5ZaaHACoE+2Z87T0g2A==" - }, - "@sentry/utils": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.1.tgz", - "integrity": "sha512-FOhWxASvIQREAlSuWf3Vmb4uIkG0fmRdHkULpuv5dFmrMX2PpudYAppQtS8K9V4BYxFy6KFdUht1Qz5zYTecMw==", - "requires": { - "@sentry/types": "5.29.1", - "tslib": "^1.9.3" - } - } } }, "@sentry/core": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.1.tgz", - "integrity": "sha512-SMybIx9IlswkJ7a61ez/zjdiMdAo51Adpo4nVrzke2k84U/t726/EbJj0FJ4vVgsGdLCvSSZ6v7BQlINcwWupg==", - "requires": { - "@sentry/hub": "5.29.1", - "@sentry/minimal": "5.29.1", - "@sentry/types": "5.29.1", - "@sentry/utils": "5.29.1", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.2.tgz", + "integrity": "sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw==", + "requires": { + "@sentry/hub": "5.29.2", + "@sentry/minimal": "5.29.2", + "@sentry/types": "5.29.2", + "@sentry/utils": "5.29.2", "tslib": "^1.9.3" - }, - "dependencies": { - "@sentry/types": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.1.tgz", - "integrity": "sha512-QXZBA1gJheMYTGFV+UUhr3+jKpGZqPx8kEJABs8htlKabCDJlEeoFNmeqPuVxCxukoy5ZaaHACoE+2Z87T0g2A==" - }, - "@sentry/utils": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.1.tgz", - "integrity": "sha512-FOhWxASvIQREAlSuWf3Vmb4uIkG0fmRdHkULpuv5dFmrMX2PpudYAppQtS8K9V4BYxFy6KFdUht1Qz5zYTecMw==", - "requires": { - "@sentry/types": "5.29.1", - "tslib": "^1.9.3" - } - } } }, "@sentry/hub": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.1.tgz", - "integrity": "sha512-Ig/vqCiJcsnGaWajkWRFH+5IKeo50ZtsjM0zJb8IfTadLjQuF/gTQst0aXO3l6q4HzveeGsELY8jlm6WVcq9Aw==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.2.tgz", + "integrity": "sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==", "requires": { - "@sentry/types": "5.29.1", - "@sentry/utils": "5.29.1", + "@sentry/types": "5.29.2", + "@sentry/utils": "5.29.2", "tslib": "^1.9.3" - }, - "dependencies": { - "@sentry/types": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.1.tgz", - "integrity": "sha512-QXZBA1gJheMYTGFV+UUhr3+jKpGZqPx8kEJABs8htlKabCDJlEeoFNmeqPuVxCxukoy5ZaaHACoE+2Z87T0g2A==" - }, - "@sentry/utils": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.1.tgz", - "integrity": "sha512-FOhWxASvIQREAlSuWf3Vmb4uIkG0fmRdHkULpuv5dFmrMX2PpudYAppQtS8K9V4BYxFy6KFdUht1Qz5zYTecMw==", - "requires": { - "@sentry/types": "5.29.1", - "tslib": "^1.9.3" - } - } } }, "@sentry/integrations": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.29.0.tgz", - "integrity": "sha512-SGqpi1Qd1a7gGL6aYrJnKqU/DNJcHvnhD3qOgow23ivEpaJv1BtQSKxv17IbO/CIFn3A0o1a18wY6xef9isKEQ==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-5.29.2.tgz", + "integrity": "sha512-bH50B0xubbHrJFq8xZRxOc5BgXe1PXKfC0OqQkhhSd+Bu2WDLCHcn0CEzV+8thZTYkipAoFAFJNdEWcsM2Wcew==", "requires": { - "@sentry/types": "5.29.0", - "@sentry/utils": "5.29.0", + "@sentry/types": "5.29.2", + "@sentry/utils": "5.29.2", "localforage": "1.8.1", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.1.tgz", - "integrity": "sha512-lAa3+Duxum1qQvR0tKiBUsH6Ehit3g/vO53SqBib7YK3qdvIUWHacmkJvfz/AeSvVnpJ9bsBMCVRJNSVe8BPVA==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.2.tgz", + "integrity": "sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==", "requires": { - "@sentry/hub": "5.29.1", - "@sentry/types": "5.29.1", + "@sentry/hub": "5.29.2", + "@sentry/types": "5.29.2", "tslib": "^1.9.3" - }, - "dependencies": { - "@sentry/types": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.1.tgz", - "integrity": "sha512-QXZBA1gJheMYTGFV+UUhr3+jKpGZqPx8kEJABs8htlKabCDJlEeoFNmeqPuVxCxukoy5ZaaHACoE+2Z87T0g2A==" - } } }, "@sentry/types": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.0.tgz", - "integrity": "sha512-iDkxT/9sT3UF+Xb+JyLjZ5caMXsgLfRyV9VXQEiR2J6mgpMielj184d9jeF3bm/VMuAf/VFFqrHlcVsVgmrrMw==" + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.2.tgz", + "integrity": "sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA==" }, "@sentry/utils": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.0.tgz", - "integrity": "sha512-b2B1gshw2u3EHlAi84PuI5sfmLKXW1z9enMMhNuuNT/CoRp+g5kMAcUv/qYTws7UNnYSvTuVGuZG30v1e0hP9A==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.2.tgz", + "integrity": "sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==", "requires": { - "@sentry/types": "5.29.0", + "@sentry/types": "5.29.2", "tslib": "^1.9.3" } }, @@ -4099,9 +4269,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.165", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", - "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==", + "version": "4.14.166", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.166.tgz", + "integrity": "sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA==", "dev": true }, "@types/mdast": { @@ -4451,13 +4621,13 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.1.tgz", - "integrity": "sha512-pQZtXupCn11O4AwpYVUX4PDFfmIJl90ZgrEBg0CEcqlwvPiG0uY81fimr1oMFblZnpKAq6prrT9a59pj1x58rw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.0.tgz", + "integrity": "sha512-x4arJMXBxyD6aBXLm3W7mSDZRiABzy+2PCLJbL7OPqlp53VXhaA1HKK7R2rTee5OlRhnUgnp8lZyVIqjnyPT6g==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.0.1", - "@typescript-eslint/scope-manager": "4.0.1", + "@typescript-eslint/experimental-utils": "4.11.0", + "@typescript-eslint/scope-manager": "4.11.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -4465,19 +4635,99 @@ "tsutils": "^3.17.1" }, "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.0.tgz", + "integrity": "sha512-1VC6mSbYwl1FguKt8OgPs8xxaJgtqFpjY/UzUYDBKq4pfQ5lBvN2WVeqYkzf7evW42axUHYl2jm9tNyFsb8oLg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.11.0", + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/typescript-estree": "4.11.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.11.0.tgz", + "integrity": "sha512-6VSTm/4vC2dHM3ySDW9Kl48en+yLNfVV6LECU8jodBHQOhO8adAVizaZ1fV0QGZnLQjQ/y0aBj5/KXPp2hBTjA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/visitor-keys": "4.11.0" + } + }, + "@typescript-eslint/types": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.11.0.tgz", + "integrity": "sha512-XXOdt/NPX++txOQHM1kUMgJUS43KSlXGdR/aDyEwuAEETwuPt02Nc7v+s57PzuSqMbNLclblQdv3YcWOdXhQ7g==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.0.tgz", + "integrity": "sha512-eA6sT5dE5RHAFhtcC+b5WDlUIGwnO9b0yrfGa1mIOIAjqwSQCpXbLiFmKTdRbQN/xH2EZkGqqLDrKUuYOZ0+Hg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/visitor-keys": "4.11.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.0.tgz", + "integrity": "sha512-tRYKyY0i7cMk6v4UIOCjl1LhuepC/pc6adQqJk4Is3YcC6k46HvsV9Wl7vQoLbm9qADgeujiT7KdLrylvFIQ+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.11.0", + "eslint-visitor-keys": "^2.0.0" + } + }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" } }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } @@ -4497,41 +4747,41 @@ } }, "@typescript-eslint/parser": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.10.0.tgz", - "integrity": "sha512-amBvUUGBMadzCW6c/qaZmfr3t9PyevcSWw7hY2FuevdZVp5QPw/K76VSQ5Sw3BxlgYCHZcK6DjIhSZK0PQNsQg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.11.0.tgz", + "integrity": "sha512-NBTtKCC7ZtuxEV5CrHUO4Pg2s784pvavc3cnz6V+oJvVbK4tH9135f/RBP6eUA2KHiFKAollSrgSctQGmHbqJQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.10.0", - "@typescript-eslint/types": "4.10.0", - "@typescript-eslint/typescript-estree": "4.10.0", + "@typescript-eslint/scope-manager": "4.11.0", + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/typescript-estree": "4.11.0", "debug": "^4.1.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.10.0.tgz", - "integrity": "sha512-WAPVw35P+fcnOa8DEic0tQUhoJJsgt+g6DEcz257G7vHFMwmag58EfowdVbiNcdfcV27EFR0tUBVXkDoIvfisQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.11.0.tgz", + "integrity": "sha512-6VSTm/4vC2dHM3ySDW9Kl48en+yLNfVV6LECU8jodBHQOhO8adAVizaZ1fV0QGZnLQjQ/y0aBj5/KXPp2hBTjA==", "dev": true, "requires": { - "@typescript-eslint/types": "4.10.0", - "@typescript-eslint/visitor-keys": "4.10.0" + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/visitor-keys": "4.11.0" } }, "@typescript-eslint/types": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.10.0.tgz", - "integrity": "sha512-+dt5w1+Lqyd7wIPMa4XhJxUuE8+YF+vxQ6zxHyhLGHJjHiunPf0wSV8LtQwkpmAsRi1lEOoOIR30FG5S2HS33g==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.11.0.tgz", + "integrity": "sha512-XXOdt/NPX++txOQHM1kUMgJUS43KSlXGdR/aDyEwuAEETwuPt02Nc7v+s57PzuSqMbNLclblQdv3YcWOdXhQ7g==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.10.0.tgz", - "integrity": "sha512-mGK0YRp9TOk6ZqZ98F++bW6X5kMTzCRROJkGXH62d2azhghmq+1LNLylkGe6uGUOQzD452NOAEth5VAF6PDo5g==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.0.tgz", + "integrity": "sha512-eA6sT5dE5RHAFhtcC+b5WDlUIGwnO9b0yrfGa1mIOIAjqwSQCpXbLiFmKTdRbQN/xH2EZkGqqLDrKUuYOZ0+Hg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.10.0", - "@typescript-eslint/visitor-keys": "4.10.0", + "@typescript-eslint/types": "4.11.0", + "@typescript-eslint/visitor-keys": "4.11.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -4541,12 +4791,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.10.0.tgz", - "integrity": "sha512-hPyz5qmDMuZWFtHZkjcCpkAKHX8vdu1G3YsCLEd25ryZgnJfj6FQuJ5/O7R+dB1ueszilJmAFMtlU4CA6se3Jg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.0.tgz", + "integrity": "sha512-tRYKyY0i7cMk6v4UIOCjl1LhuepC/pc6adQqJk4Is3YcC6k46HvsV9Wl7vQoLbm9qADgeujiT7KdLrylvFIQ+A==", "dev": true, "requires": { - "@typescript-eslint/types": "4.10.0", + "@typescript-eslint/types": "4.11.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -4659,9 +4909,9 @@ } }, "@uirouter/core": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.6.tgz", - "integrity": "sha512-+i1iNaPQRp1JoEnxi4EsTX30bSvkWtPEenENWdvdIzX615Q3WCWwnbW9DtrwX9gSD5qmWEaQEtGMU0uF3qJ+1g==" + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz", + "integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w==" }, "@webassemblyjs/ast": { "version": "1.9.0", @@ -5066,11 +5316,11 @@ "integrity": "sha512-yzcHpPMLQl0232nDzm5P4iAFTFQ9dMw0QgFLuKYbDj9M0xJ62z0oudYD/Lvh1pWfRsukiytP4Xj6BHOSrSXP8A==" }, "angular-ui-router": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-1.0.28.tgz", - "integrity": "sha512-BhcaW/ac+6/Ps9to9tHEuCG3LHgatD5mL0a6WRie4mBxdOjaEqNMB6SIB0l7qNpJMhMsi3m2SRMknk52rZCQQg==", + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-1.0.29.tgz", + "integrity": "sha512-VurVxwueqEFD0JJBHkATOvLinhebc8/052jyhYrFVbHtWEc//IX4Wr55WvmJoTZJ6lnQ3nBlpX9VBKS5wiRsOg==", "requires": { - "@uirouter/core": "6.0.6" + "@uirouter/core": "6.0.7" } }, "ansi-colors": { @@ -5343,9 +5593,9 @@ "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -5454,9 +5704,9 @@ "integrity": "sha512-ZUWFfYy9Mu5ppoDJr3TxY0UWc4peP3tS5sStgnKkRh3urYalQaZWHewhbDbz40I84PO0xhVdBGut1xQtjum3mw==" }, "aws-sdk": { - "version": "2.805.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.805.0.tgz", - "integrity": "sha512-mnIiHWp541pappZPJs+P6bx18yIcJTTr4eu2n0aF9+MY4UteuFWRu0fGLssYnDwCHvtzN7/j776Pd/1QXy9hqg==", + "version": "2.818.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.818.0.tgz", + "integrity": "sha512-xxIEdaKXtrEZNVUvKiW8MRj1Ux2tH/ubDLl8+tnrfhNOJbq5u5FfD56Kui4Ca+CG4xamQ5LaBktVYWAEyaMtUw==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -5497,17 +5747,17 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, "axios": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", - "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { "follow-redirects": "^1.10.0" }, "dependencies": { "follow-redirects": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" } } }, @@ -5521,308 +5771,18 @@ "is-buffer": "^2.0.3" } }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - } - } - }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "dev": true, - "requires": { - "babel-helper-bindify-decorators": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" } }, "babel-jest": { @@ -5990,24 +5950,6 @@ } } }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -6042,498 +5984,78 @@ "@types/babel__traverse": "^7.0.6" } }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", - "dev": true - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-generators": "^6.5.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "dev": true, - "requires": { - "babel-helper-explode-class": "^6.24.1", - "babel-plugin-syntax-decorators": "^6.13.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "dev": true, - "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "dev": true, - "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "dev": true, - "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "babel-plugin-module-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz", + "integrity": "sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==", "dev": true, "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" + "find-babel-config": "^1.2.0", + "glob": "^7.1.6", + "pkg-up": "^3.1.0", + "reselect": "^4.0.0", + "resolve": "^1.13.1" }, "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "jsesc": "~0.5.0" + "p-limit": "^2.0.0" } - } - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "dev": true, - "requires": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-for-of-as-array": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-for-of-as-array/-/babel-plugin-transform-for-of-as-array-1.1.1.tgz", - "integrity": "sha512-eE4hZJhOUKpX0q/X3adR8B4hLox+t8oe4ZqmhANUmv4cds07AbWt6O0rtFXK7PKFPPnW4nz/5mpbkPMkflyGeg==", - "dev": true - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "dev": true, - "requires": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-self": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "dev": true, - "requires": { - "regenerator-transform": "^0.10.0" - }, - "dependencies": { - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", "dev": true, "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" + "find-up": "^3.0.0" } } } }, - "babel-plugin-transform-runtime": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", - "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true }, "babel-preset-current-node-syntax": { "version": "1.0.0", @@ -6555,65 +6077,6 @@ "@babel/plugin-syntax-top-level-await": "^7.8.3" } }, - "babel-preset-env": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", - "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.23.0", - "babel-plugin-transform-es2015-classes": "^6.23.0", - "babel-plugin-transform-es2015-computed-properties": "^6.22.0", - "babel-plugin-transform-es2015-destructuring": "^6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", - "babel-plugin-transform-es2015-for-of": "^6.23.0", - "babel-plugin-transform-es2015-function-name": "^6.22.0", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-umd": "^6.23.0", - "babel-plugin-transform-es2015-object-super": "^6.22.0", - "babel-plugin-transform-es2015-parameters": "^6.23.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", - "babel-plugin-transform-exponentiation-operator": "^6.22.0", - "babel-plugin-transform-regenerator": "^6.22.0", - "browserslist": "^3.2.6", - "invariant": "^2.2.2", - "semver": "^5.3.0" - }, - "dependencies": { - "browserslist": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", - "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" - } - } - } - }, - "babel-preset-flow": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", - "dev": true, - "requires": { - "babel-plugin-transform-flow-strip-types": "^6.22.0" - } - }, "babel-preset-jest": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", @@ -6624,180 +6087,23 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, - "babel-preset-react": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", - "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-plugin-transform-react-jsx-self": "^6.22.0", - "babel-plugin-transform-react-jsx-source": "^6.22.0", - "babel-preset-flow": "^6.23.0" - } - }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "dev": true, - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-preset-stage-3": "^6.24.1" - } - }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "dev": true, - "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-exponentiation-operator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "babel-runtime": { + "version": "5.8.38", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz", + "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=", "dev": true, "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" + "core-js": "^1.0.0" }, "dependencies": { "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", "dev": true - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } } } }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - } - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -7077,9 +6383,9 @@ "dev": true }, "boxicons": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/boxicons/-/boxicons-1.8.0.tgz", - "integrity": "sha512-B/CxD/2a23rK+35rzbsdm1FiW/Ka1gfXZjaDZXSDRz8C8p94piUSP1L4ADjx62CwafM9STiPLXoJiSriUq4azg==" + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/boxicons/-/boxicons-1.8.1.tgz", + "integrity": "sha512-iQVGukTpmwvIQiWr1vtMW6nsndlE7TFGjgOAx/7nJzUo29qpKZfXg7jntbQX2fH4v7yoEXvHGlL23sOi5kzNvQ==" }, "brace-expansion": { "version": "1.1.11", @@ -8662,38 +7968,38 @@ "dev": true }, "core-js-compat": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.0.tgz", - "integrity": "sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.1.tgz", + "integrity": "sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==", "dev": true, "requires": { - "browserslist": "^4.14.7", + "browserslist": "^4.15.0", "semver": "7.0.0" }, "dependencies": { "browserslist": { - "version": "4.14.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", - "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz", + "integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001157", + "caniuse-lite": "^1.0.30001165", "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.591", + "electron-to-chromium": "^1.3.621", "escalade": "^3.1.1", - "node-releases": "^1.1.66" + "node-releases": "^1.1.67" } }, "caniuse-lite": { - "version": "1.0.30001161", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001161.tgz", - "integrity": "sha512-JharrCDxOqPLBULF9/SPa6yMcBRTjZARJ6sc3cuKrPfyIk64JN6kuMINWqA99Xc8uElMFcROliwtz0n9pYej+g==", + "version": "1.0.30001170", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz", + "integrity": "sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==", "dev": true }, "electron-to-chromium": { - "version": "1.3.607", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.607.tgz", - "integrity": "sha512-h2SYNaBnlplGS0YyXl8oJWokfcNxVjJANQfMCsQefG6OSuAuNIeW+A8yGT/ci+xRoBb3k2zq1FrOvkgoKBol8g==", + "version": "1.3.631", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.631.tgz", + "integrity": "sha512-mPEG/52142po0XK1jQkZtbMmp38MZtQ3JDFItYxV65WXyhxDYEQ54tP4rb93m0RbMlZqQ+4zBw2N7UumSgGfbA==", "dev": true }, "node-releases": { @@ -8816,6 +8122,12 @@ "sha.js": "^2.4.8" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -9279,12 +8591,6 @@ "pinkie-promise": "^2.0.0" } }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -9757,15 +9063,6 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -10424,13 +9721,13 @@ } }, "eslint": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz", - "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz", + "integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.2.1", + "@eslint/eslintrc": "^0.2.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -10440,10 +9737,10 @@ "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.0", + "espree": "^7.3.1", "esquery": "^1.2.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", "globals": "^12.1.0", @@ -10463,7 +9760,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^5.2.3", + "table": "^6.0.4", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -10592,6 +9889,567 @@ "type-check": "~0.4.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", + "integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-angular": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-angular/-/eslint-plugin-angular-4.0.1.tgz", + "integrity": "sha512-OaW5G461C2lIkOG+/bhnBoXB9UQm/r0Dj2Qf9uiIN0/ncvf2Llp30L0q1tqWkN8/CxyBwQKh1v0hpCLLDjaIKQ==", + "dev": true + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + } + } + }, + "eslint-plugin-jest": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz", + "integrity": "sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^4.0.1" + } + }, + "eslint-plugin-prettier": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz", + "integrity": "sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-simple-import-sort": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-7.0.0.tgz", + "integrity": "sha512-U3vEDB5zhYPNfxT5TYR7u01dboFZp+HNpnGhkDB2g/2E4wZ/g1Q9Ton8UwCLfRV9yAKyYqDh62oHOamvkFxsvw==", + "dev": true + }, + "eslint-plugin-typesafe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-typesafe/-/eslint-plugin-typesafe-0.2.0.tgz", + "integrity": "sha512-i/+58OWHWDxUDf5/XLyPHULpftYSX9BsBpG/t9oS34PyIO+vtkY2TbpoLamiM7hJuyDImhTJeZM3uKl53iqivQ==", + "dev": true, + "requires": { + "babel-eslint": "^10.1.0", + "eslint": "^7.16.0", + "requireindex": "^1.2.0" + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "eslint": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz", + "integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -10619,10 +10477,13 @@ "dev": true }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "shebang-command": { "version": "2.0.0", @@ -10639,6 +10500,28 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -10663,215 +10546,50 @@ "has-flag": "^4.0.0" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", - "integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ==", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", - "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.13.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", - "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } - } - }, - "eslint-plugin-angular": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-angular/-/eslint-plugin-angular-4.0.1.tgz", - "integrity": "sha512-OaW5G461C2lIkOG+/bhnBoXB9UQm/r0Dj2Qf9uiIN0/ncvf2Llp30L0q1tqWkN8/CxyBwQKh1v0hpCLLDjaIKQ==", - "dev": true - }, - "eslint-plugin-import": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", - "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "array.prototype.flat": "^1.2.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.4", - "eslint-module-utils": "^2.6.0", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.1", - "read-pkg-up": "^2.0.0", - "resolve": "^1.17.0", - "tsconfig-paths": "^3.9.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "table": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz", + "integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "ajv": "^6.12.4", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "pify": "^2.0.0" + "prelude-ls": "^1.2.1" } }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "isexe": "^2.0.0" } }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "eslint-plugin-jest": { - "version": "24.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.3.tgz", - "integrity": "sha512-dNGGjzuEzCE3d5EPZQ/QGtmlMotqnYWD/QpCZ1UuZlrMAdhG5rldh0N0haCvhGnUkSeuORS5VNROwF9Hrgn3Lg==", - "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^4.0.1" - } - }, - "eslint-plugin-prettier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", - "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-plugin-simple-import-sort": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-6.0.1.tgz", - "integrity": "sha512-RfFnoi7fQtv7z9sZNJidIcZgWc0ZJe8uOPC3ldmatai4Igr5iDpzTmSUDEZKYm4TnrR01N0X32kfKvax7bivHQ==", - "dev": true - }, "eslint-scope": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", @@ -10898,22 +10616,22 @@ "dev": true }, "esotope-hammerhead": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.5.6.tgz", - "integrity": "sha512-E5cJKuecAEt/psafJ8rBjAezBS5/5iqyzgzRPH2JWEhZYTRzkphX9c9djqpJj8Zcfv7PpqgNgXXbaqxgCIVsPw==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.5.7.tgz", + "integrity": "sha512-5aDxLOFlwjwq4UH/aOGXZlSR0nyBJSTsSpdgcDs6DMjpcMxT+uaDRl1ZbQuIa57E5blHR5/Qt6X8EdXYCq10QA==", "dev": true, "requires": { "@types/estree": "^0.0.39" } }, "espree": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", - "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { "acorn": "^7.4.0", - "acorn-jsx": "^5.2.0", + "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^1.3.0" }, "dependencies": { @@ -11512,12 +11230,12 @@ } }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-loader": { @@ -11595,6 +11313,24 @@ } } }, + "find-babel-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", + "dev": true, + "requires": { + "json5": "^0.5.1", + "path-exists": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + } + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -11754,31 +11490,19 @@ } }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", "dev": true }, "flush-write-stream": { @@ -12003,6 +11727,12 @@ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -12622,16 +12352,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -13283,15 +13003,6 @@ "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-12.4.0.tgz", "integrity": "sha512-tVXt9QcczIp9Y6yWxDf6p8+4YfmU1NGSbNU3kxY4EPY+QV++j4DlC1gZ/wkNIi8oTpyJUWZKqzynCF9Y135weQ==" }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -16106,15 +15817,6 @@ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -17131,9 +16833,9 @@ "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=" }, "mongoose": { - "version": "5.11.8", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.8.tgz", - "integrity": "sha512-RRfrYLg7pyuyx7xu5hwadjIZZJB9W2jqIMkL1CkTmk/uOCX3MX2tl4BVIi2rJUtgMNwn6dy3wBD3soB8I9Nlog==", + "version": "5.11.9", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.9.tgz", + "integrity": "sha512-lmG6R64jtGGxqtn88BkkY+v470LUfGgyTKUyjswQ5c01GNgQvxA0kQd8h+tm0hZb639hKNRxL9ZBQlLleUpuIQ==", "requires": { "@types/mongodb": "^3.5.27", "bson": "^1.1.4", @@ -17393,9 +17095,9 @@ "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" }, "neverthrow": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-3.0.0.tgz", - "integrity": "sha512-mjfDK9npBJZtHnpiSIi58z8MKoEDk25MRZesIoxtmZFDv3nNWMZnuqXigHCAfwHiZ67klmcuIIUfKHaOrNaQhQ==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-3.1.2.tgz", + "integrity": "sha512-LK8IjLdwPiivKf13tzG9PMlOdS27I1y3K8gGyJImj8yeTS0kMltOwnw9v5ZAbybahdAn2phbyZ+fY5yeukDq/g==" }, "ng-infinite-scroll": { "version": "1.3.0", @@ -17419,9 +17121,9 @@ } }, "ngrok": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-3.3.0.tgz", - "integrity": "sha512-NuTfuxttodW6o9cPEKiJgjmjNzolKHoUPaK7I4SbaJVkHd4cuUTlv9YsXNzBFrcNFSsDLuCyCpjsaqe1OvIPSQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-3.4.0.tgz", + "integrity": "sha512-fwgYlGQsLclMRVkGNhdWbeE5xx/xCoKr8zHoqRehOxGu4Ep14dAfe1u45rE0rqtUhWqTVfCl1lWV2rLUNizIDw==", "dev": true, "requires": { "@types/node": "^8.10.50", @@ -17746,9 +17448,9 @@ "dev": true }, "nodemailer": { - "version": "6.4.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.16.tgz", - "integrity": "sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ==" + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ==" }, "nodemailer-direct-transport": { "version": "3.3.2", @@ -20181,11 +19883,23 @@ "semver": "^5.1.0" } }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "dev": true + }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", @@ -20792,15 +20506,15 @@ } }, "sinon": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.1.tgz", - "integrity": "sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.2.tgz", + "integrity": "sha512-9Owi+RisvCZpB0bdOVFfL314I6I4YoRlz6Isi4+fr8q8YQsDPoCe5UnmNtKHRThX3negz2bXHWIuiPa42vM8EQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.1", "@sinonjs/fake-timers": "^6.0.1", "@sinonjs/formatio": "^5.0.1", - "@sinonjs/samsam": "^5.2.0", + "@sinonjs/samsam": "^5.3.0", "diff": "^4.0.2", "nise": "^4.0.4", "supports-color": "^7.1.0" @@ -20845,20 +20559,44 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true } } @@ -22113,53 +21851,47 @@ "dev": true }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz", + "integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^6.12.4", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -22380,22 +22112,28 @@ } }, "testcafe": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.9.4.tgz", - "integrity": "sha512-tiG0a47vvwuI6xle002wQitpEGG3Oi+qJOjFAgXT7rPYzcHrPRYP2rgefvCw/VE/qsGRHzyvDDYH3FbQMojDAA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.10.1.tgz", + "integrity": "sha512-c1ErVj+QvQR7fqJs/XVa4S7Ctl5Hf5bxnkTaFVl4Qt2QlliL3EAXtL3WjCOx65Pd8BatxTWUCXJ5NVwAvq7jWg==", "dev": true, "requires": { + "@babel/core": "^7.12.1", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-flow": "^7.12.1", + "@babel/preset-react": "^7.12.1", "@types/node": "^10.12.19", "async-exit-hook": "^1.1.2", - "babel-core": "^6.22.1", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-for-of-as-array": "^1.1.1", - "babel-plugin-transform-runtime": "^6.22.0", - "babel-preset-env": "^1.1.8", - "babel-preset-flow": "^6.23.0", - "babel-preset-react": "^6.24.1", - "babel-preset-stage-2": "^6.22.0", - "babel-runtime": "^6.22.0", + "babel-plugin-module-resolver": "^4.0.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", "bin-v8-flags-filter": "^1.1.2", "bowser": "^2.8.1", "callsite": "^1.0.0", @@ -22447,10 +22185,11 @@ "resolve-cwd": "^1.0.0", "resolve-from": "^4.0.0", "sanitize-filename": "^1.6.0", + "semver": "^5.6.0", "source-map-support": "^0.5.16", "strip-bom": "^2.0.0", "testcafe-browser-tools": "2.0.13", - "testcafe-hammerhead": "17.1.20", + "testcafe-hammerhead": "18.0.0", "testcafe-legacy-api": "4.0.0", "testcafe-reporter-json": "^2.1.0", "testcafe-reporter-list": "^2.1.0", @@ -22459,8 +22198,9 @@ "testcafe-reporter-xunit": "^2.1.0", "time-limit-promise": "^1.0.2", "tmp": "0.0.28", - "tree-kill": "^1.1.0", - "typescript": "^3.3.3" + "tree-kill": "^1.2.2", + "typescript": "^3.3.3", + "unquote": "^1.1.1" }, "dependencies": { "@nodelib/fs.stat": { @@ -22470,9 +22210,9 @@ "dev": true }, "@types/node": { - "version": "10.17.44", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.44.tgz", - "integrity": "sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw==", + "version": "10.17.50", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.50.tgz", + "integrity": "sha512-vwX+/ija9xKc/z9VqMCdbf4WYcMTGsI0I/L/6shIF3qXURxZOhPQlPRHtjTpiNhAwn0paMJzlOQqw6mAGEQnTA==", "dev": true }, "array-union": { @@ -22633,12 +22373,6 @@ } } }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -22775,6 +22509,14 @@ "dev": true, "requires": { "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "micromatch": { @@ -22869,12 +22611,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -23118,9 +22854,9 @@ } }, "testcafe-hammerhead": { - "version": "17.1.20", - "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-17.1.20.tgz", - "integrity": "sha512-1Z1r+cUjn0Cg3/Q8uwUYGeL+UvKPXAtYtiOqdGKM8/nXxndxc+q3KBvmBZXSUE9rAWhn3Mb3SQ8LoSw1+iQyiA==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-18.0.0.tgz", + "integrity": "sha512-qtyzxo1TuIJJ0S2z5Cy+GRHv5V3o6HQlKfH1iv3/U1qJQNs53sRYtZ3+B5b/ddefPbCF5opZdY6uLhhBlj8X/w==", "dev": true, "requires": { "acorn-hammerhead": "^0.3.0", @@ -23130,7 +22866,7 @@ "crypto-md5": "^1.0.0", "css": "2.2.3", "debug": "4.1.1", - "esotope-hammerhead": "0.5.6", + "esotope-hammerhead": "0.5.7", "iconv-lite": "0.5.1", "lodash": "^4.17.19", "lru-cache": "2.6.3", @@ -23201,9 +22937,9 @@ "dev": true }, "nanoid": { - "version": "3.1.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz", - "integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==", + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", "dev": true }, "parse5": { @@ -23262,21 +22998,6 @@ "integrity": "sha1-rT83PZJJrjJIgVZVgryQ4VKrvWg=", "dev": true }, - "babel-runtime": { - "version": "5.8.38", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz", - "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=", - "dev": true, - "requires": { - "core-js": "^1.0.0" - } - }, - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", - "dev": true - }, "dedent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.6.0.tgz", @@ -23593,12 +23314,6 @@ "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", "dev": true }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -23695,12 +23410,13 @@ "dev": true }, "ts-node": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", - "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", "dev": true, "requires": { "arg": "^4.1.0", + "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "source-map-support": "^0.5.17", @@ -23708,9 +23424,9 @@ } }, "ts-node-dev": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-1.0.0.tgz", - "integrity": "sha512-leA/3TgGtnVU77fGngBwVZztqyDRXirytR7dMtMWZS5b2hGpLl+VDnB0F/gf3A+HEPSzS/KwxgXFP7/LtgX4MQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-1.1.1.tgz", + "integrity": "sha512-kAO8LUZgXZSY0+PucMPsQ0Bbdv0x+lgbN7j8gcD4PuTI4uKC6YchekaspmYTBNilkiu+rQYkWJA7cK+Q8/B0tQ==", "dev": true, "requires": { "chokidar": "^3.4.0", @@ -25210,9 +24926,9 @@ } }, "winston-cloudwatch": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/winston-cloudwatch/-/winston-cloudwatch-2.4.0.tgz", - "integrity": "sha512-mODEWN2lK83RGx2BFUuHoxo6Aam2I36ZEUz7lz2K4V8dvYACRqy1OXyql2wL9Qs8IBSwU0XnC7XPERxiD4XWEg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/winston-cloudwatch/-/winston-cloudwatch-2.5.0.tgz", + "integrity": "sha512-J1h7f1uY+gghyYVYG6SjheIrXvnGbhFUFIyXHQ34cWl1hXUWGu6u7Kik1DpUCkTlZ0sWowVc2Sh+ZI9PEvaZNw==", "requires": { "async": "^3.1.0", "aws-sdk": "^2.553.0", @@ -25412,15 +25128,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", diff --git a/package.json b/package.json index 6825005ca8..b3da592038 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "4.51.0", + "version": "4.52.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -65,11 +65,11 @@ "@opengovsg/angular-legacy-sortablejs-maintained": "^1.0.0", "@opengovsg/angular-recaptcha-fallback": "^5.0.0", "@opengovsg/formsg-sdk": "0.8.2", - "@opengovsg/myinfo-gov-client": "^2.1.2", - "@opengovsg/ng-file-upload": "^12.2.14", + "@opengovsg/myinfo-gov-client": "^2.1.3", + "@opengovsg/ng-file-upload": "^12.2.15", "@opengovsg/spcp-auth-client": "^1.4.0", - "@sentry/browser": "^5.29.1", - "@sentry/integrations": "^5.29.0", + "@sentry/browser": "^5.29.2", + "@sentry/integrations": "^5.29.2", "@stablelib/base64": "^1.0.0", "JSONStream": "^1.3.5", "angular": "~1.8.2", @@ -85,16 +85,16 @@ "angular-translate": "^2.18.3", "angular-translate-loader-partial": "^2.18.3", "angular-ui-bootstrap": "~2.5.6", - "angular-ui-router": "~1.0.28", + "angular-ui-router": "~1.0.29", "await-to-js": "^2.1.1", "aws-info": "^1.1.0", - "aws-sdk": "^2.805.0", - "axios": "^0.21.0", + "aws-sdk": "^2.818.0", + "axios": "^0.21.1", "bcrypt": "^5.0.0", "bluebird": "^3.5.2", "body-parser": "^1.18.3", "bootstrap": "3.4.1", - "boxicons": "1.8.0", + "boxicons": "1.8.1", "bson-ext": "^2.0.5", "busboy": "^0.3.1", "celebrate": "^13.0.4", @@ -130,15 +130,15 @@ "lodash": "^4.17.20", "moment-timezone": "0.5.32", "mongodb-uri": "^0.9.7", - "mongoose": "^5.11.8", + "mongoose": "^5.11.9", "multiparty": ">=4.2.2", - "neverthrow": "^3.0.0", + "neverthrow": "^3.1.2", "ng-infinite-scroll": "^1.3.0", "ng-table": "^3.0.1", "ngclipboard": "^2.0.0", "nocache": "^2.1.0", "node-cache": "^5.1.2", - "nodemailer": "^6.4.16", + "nodemailer": "^6.4.17", "nodemailer-direct-transport": "~3.3.2", "opossum": "^5.1.1", "promise-retry": "^2.0.1", @@ -158,13 +158,13 @@ "web-streams-polyfill": "^3.0.1", "whatwg-fetch": "^3.5.0", "winston": "^3.3.3", - "winston-cloudwatch": "^2.4.0" + "winston-cloudwatch": "^2.5.0" }, "devDependencies": { "@babel/core": "^7.12.10", - "@babel/plugin-transform-runtime": "^7.12.1", - "@babel/preset-env": "^7.12.7", - "@opengovsg/mockpass": "^2.6.0", + "@babel/plugin-transform-runtime": "^7.12.10", + "@babel/preset-env": "^7.12.11", + "@opengovsg/mockpass": "^2.6.1", "@types/bcrypt": "^3.0.0", "@types/bluebird": "^3.5.33", "@types/busboy": "^0.2.3", @@ -196,8 +196,8 @@ "@types/uid-generator": "^2.0.2", "@types/uuid": "^8.3.0", "@types/validator": "^13.1.0", - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.10.0", + "@typescript-eslint/eslint-plugin": "^4.11.0", + "@typescript-eslint/parser": "^4.11.0", "auto-changelog": "^2.2.1", "axios-mock-adapter": "^1.19.0", "babel-loader": "^8.2.2", @@ -208,13 +208,14 @@ "css-loader": "^2.1.1", "csv-parse": "^4.14.2", "env-cmd": "^10.1.0", - "eslint": "^7.14.0", + "eslint": "^7.16.0", "eslint-config-prettier": "^7.0.0", "eslint-plugin-angular": "^4.0.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-prettier": "^3.1.3", - "eslint-plugin-simple-import-sort": "^6.0.1", + "eslint-plugin-prettier": "^3.3.0", + "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-typesafe": "^0.2.0", "form-data": "^3.0.0", "google-fonts-plugin": "4.1.0", "html-loader": "~0.5.5", @@ -231,13 +232,13 @@ "mockdate": "^3.0.2", "mockingoose": "^2.13.2", "mongodb-memory-server-core": "^6.9.2", - "ngrok": "^3.3.0", + "ngrok": "^3.4.0", "optimize-css-assets-webpack-plugin": "^5.0.1", "prettier": "^2.2.1", "proxyquire": "^2.1.3", "regenerator": "^0.14.4", "rimraf": "^3.0.2", - "sinon": "^9.2.1", + "sinon": "^9.2.2", "stylelint": "^13.8.0", "stylelint-config-prettier": "^8.0.2", "stylelint-config-standard": "^20.0.0", @@ -245,12 +246,12 @@ "supertest": "^6.0.1", "supertest-session": "^4.1.0", "terser-webpack-plugin": "^1.2.3", - "testcafe": "^1.9.4", + "testcafe": "^1.10.1", "ts-jest": "^26.4.4", "ts-loader": "^7.0.5", "ts-mock-imports": "^1.3.1", - "ts-node": "^9.0.0", - "ts-node-dev": "^1.0.0", + "ts-node": "^9.1.1", + "ts-node-dev": "^1.1.1", "type-fest": "^0.20.2", "typescript": "^4.0.2", "url-loader": "^1.1.2", diff --git a/src/app/controllers/submissions.server.controller.js b/src/app/controllers/submissions.server.controller.js deleted file mode 100644 index 453f72d5ce..0000000000 --- a/src/app/controllers/submissions.server.controller.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict' - -const { createReqMeta } = require('../utils/request') -const logger = require('../../config/logger').createLoggerWithLabel(module) -const MailService = require('../services/mail/mail.service').default - -/** - * Injects auto-reply SMS/Email info - * @param {Object} req - the expressjs request. Will be injected with the - * objects parsed from `req.form` and `req.body.parsedResponses` - * @param {Object} req.form - the form from the middleware forms.formById - * @param {Object} req.body - the submission for the form - * @param {Object} res - the expressjs response - * @param {Object} next - the next expressjs callback, invoked once attachments - * are processed - */ -exports.injectAutoReplyInfo = function (req, res, next) { - const { form } = req - - // Lookup table - const table = (form.form_fields || []).reduce((obj, item) => { - obj[item._id] = item - return obj - }, {}) - - // Fetch autoreply data from table and inject into req - // TODO: req.body.parsedResponses does not exist for encrypted responses! - const autoReplies = (req.body.parsedResponses || []).reduce( - (obj, item) => { - switch (item.fieldType) { - case 'email': - if (table[item._id] && item.answer) { - // Email validation handled by validateSubmission middleware - const options = table[item._id].autoReplyOptions - if (options.hasAutoReply) { - // We will send autoreply emails even with responseMode = encrypt, - // but with no submission data - // schema enforces that includeFormSummary=false for encrypt forms - obj.email.push({ - email: item.answer, - subject: options.autoReplySubject, - sender: options.autoReplySender, - body: options.autoReplyMessage, - includeFormSummary: options.includeFormSummary, - }) - } - // Reply-to enabled by default - obj.replyToEmails.push(item.answer) - } - - return obj - default: - return obj - } - }, - { - email: [], - replyToEmails: [], - }, - ) - - req.autoReplyEmails = autoReplies.email - req.autoReplySms = autoReplies.sms - req.replyToEmails = autoReplies.replyToEmails - - return next() -} - -/** - * Long waterfall to send autoreply - * @param {Object} req - Express request object - * @param {Array} req.autoReplyEmails Auto-reply email fields - * @param {Object} req.form - the form - * @param {Array>} req.autoReplyData Field-value tuples for auto-replies - * @param {Object} req.submission Mongodb Submission object - * @param {Object} req.attachments - submitted attachments, parsed by - */ -const sendEmailAutoReplies = async function (req) { - const { form, attachments, autoReplyEmails, autoReplyData, submission } = req - if (autoReplyEmails.length === 0) { - return Promise.resolve() - } - - try { - return MailService.sendAutoReplyEmails({ - form, - submission, - attachments, - responsesData: autoReplyData, - autoReplyMailDatas: autoReplyEmails, - }) - } catch (err) { - logger.error({ - message: 'Failed to send autoreply emails', - meta: { - action: 'sendEmailAutoReplies', - ...createReqMeta(req), - formId: req.form._id, - submissionId: submission.id, - }, - error: err, - }) - // We do not deal with failed autoreplies - return Promise.resolve() - } -} - -exports.sendAutoReply = function (req, res) { - const { form, submission } = req - // Return the reply early to the submitter - res.json({ - message: 'Form submission successful.', - submissionId: submission.id, - }) - // We do not handle failed autoreplies - const autoReplies = [sendEmailAutoReplies(req, res)] - return Promise.all(autoReplies).catch((err) => { - logger.error({ - message: 'Error sending autoreply', - meta: { - action: 'sendAutoReply', - formId: form._id, - submissionId: submission.id, - }, - error: err, - }) - }) -} diff --git a/src/app/models/form_statistics_total.server.model.ts b/src/app/models/form_statistics_total.server.model.ts index 0c14b7a0d1..573c61effb 100644 --- a/src/app/models/form_statistics_total.server.model.ts +++ b/src/app/models/form_statistics_total.server.model.ts @@ -41,8 +41,8 @@ const compileFormStatisticsTotalModel = (db: Mongoose) => { // Hooks FormStatisticsTotalSchema.pre( 'save', - function () { - throw new Error('FormStatisticsTotal schema is read only') + function (next) { + next(new Error('FormStatisticsTotal schema is read only')) }, ) diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index ca229ad17b..aa40583ad9 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -119,6 +119,22 @@ const compileUserModel = (db: Mongoose) => { }) } + /** + * Finds the contact numbers for all given email addresses which exist in the + * User collection. + */ + UserSchema.statics.findContactNumbersByEmails = async function ( + this: IUserModel, + emails: string[], + ) { + return this.find() + .where('email') + .in(emails) + .select('email contact -_id') + .lean() + .exec() + } + return db.model(USER_SCHEMA_ID, UserSchema) } diff --git a/src/app/modules/auth/__tests__/auth.controller.spec.ts b/src/app/modules/auth/__tests__/auth.controller.spec.ts index 844538b2a9..0639c61bd0 100644 --- a/src/app/modules/auth/__tests__/auth.controller.spec.ts +++ b/src/app/modules/auth/__tests__/auth.controller.spec.ts @@ -85,7 +85,7 @@ describe('auth.controller', () => { // Assert expect(mockRes.status).toBeCalledWith(200) - expect(mockRes.json).toBeCalledWith(`OTP sent to ${VALID_EMAIL}!`) + expect(mockRes.json).toBeCalledWith(`OTP sent to ${VALID_EMAIL}`) // Services should have been invoked. expect(MockAuthService.createLoginOtp).toHaveBeenCalledTimes(1) expect(MockMailService.sendLoginOtp).toHaveBeenCalledTimes(1) diff --git a/src/app/modules/auth/__tests__/auth.routes.spec.ts b/src/app/modules/auth/__tests__/auth.routes.spec.ts index e3ceaa14a3..5aab1ad592 100644 --- a/src/app/modules/auth/__tests__/auth.routes.spec.ts +++ b/src/app/modules/auth/__tests__/auth.routes.spec.ts @@ -247,7 +247,7 @@ describe('auth.routes', () => { // Assert expect(sendLoginOtpSpy).toHaveBeenCalled() expect(response.status).toEqual(200) - expect(response.body).toEqual(`OTP sent to ${VALID_EMAIL}!`) + expect(response.body).toEqual(`OTP sent to ${VALID_EMAIL}`) }) }) @@ -545,7 +545,7 @@ describe('auth.routes', () => { jest.spyOn(MailService, 'sendLoginOtp').mockReturnValue(okAsync(true)) const response = await request.post('/auth/sendotp').send({ email }) - expect(response.body).toEqual(`OTP sent to ${email}!`) + expect(response.body).toEqual(`OTP sent to ${email}`) } const signInUser = async (email: string, otp: string) => { diff --git a/src/app/modules/auth/auth.controller.ts b/src/app/modules/auth/auth.controller.ts index 19b4dc231e..99839c600b 100644 --- a/src/app/modules/auth/auth.controller.ts +++ b/src/app/modules/auth/auth.controller.ts @@ -86,7 +86,7 @@ export const handleLoginSendOtp: RequestHandler< meta: logMeta, }) - return res.status(StatusCodes.OK).json(`OTP sent to ${email}!`) + return res.status(StatusCodes.OK).json(`OTP sent to ${email}`) }) // Step 4b: Error occurred whilst sending otp. .mapErr((error) => { diff --git a/src/app/modules/bounce/__tests__/bounce-test-helpers.ts b/src/app/modules/bounce/__tests__/bounce-test-helpers.ts index 47ed0142a7..ed6595e9e8 100644 --- a/src/app/modules/bounce/__tests__/bounce-test-helpers.ts +++ b/src/app/modules/bounce/__tests__/bounce-test-helpers.ts @@ -138,6 +138,7 @@ export const extractBounceObject = ( const extracted = pick(bounce.toObject(), [ 'formId', 'hasAutoEmailed', + 'hasAutoSmsed', 'expireAt', 'bounces', ]) diff --git a/src/app/modules/bounce/__tests__/bounce.controller.spec.ts b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts index e1850ce7c0..75244e8e47 100644 --- a/src/app/modules/bounce/__tests__/bounce.controller.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts @@ -1,17 +1,27 @@ import { ObjectId } from 'bson' import mongoose from 'mongoose' +import { errAsync, okAsync } from 'neverthrow' import { mocked } from 'ts-jest/utils' +import getFormModel from 'src/app/models/form.server.model' import getBounceModel from 'src/app/modules/bounce/bounce.model' import * as BounceService from 'src/app/modules/bounce/bounce.service' import * as FormService from 'src/app/modules/form/form.service' import { EmailType } from 'src/app/services/mail/mail.constants' -import { IBounceSchema, IEmailNotification, ISnsNotification } from 'src/types' +import { + IBounceSchema, + IEmailNotification, + IPopulatedForm, + ISnsNotification, +} from 'src/types' import dbHandler from 'tests/unit/backend/helpers/jest-db' import expressHandler from 'tests/unit/backend/helpers/jest-express' +import { DatabaseError } from '../../core/core.errors' + const Bounce = getBounceModel(mongoose) +const FormModel = getFormModel(mongoose) jest.mock('src/app/modules/bounce/bounce.service') jest.mock('src/app/modules/form/form.service') @@ -40,6 +50,16 @@ const MOCK_REQ = expressHandler.mockRequest({ }) const MOCK_RES = expressHandler.mockResponse() const MOCK_EMAIL_RECIPIENTS = ['a@email.com', 'b@email.com'] +const MOCK_CONTACTS = [ + { + email: MOCK_EMAIL_RECIPIENTS[0], + contact: '+6581234567', + }, + { + email: MOCK_EMAIL_RECIPIENTS[1], + contact: '+6581234568', + }, +] interface IMockBounce extends IBounceSchema { isCriticalBounce: jest.Mock areAllPermanentBounces: jest.Mock @@ -49,9 +69,12 @@ interface IMockBounce extends IBounceSchema { } describe('handleSns', () => { let mockBounceDoc: IMockBounce + let mockForm: IPopulatedForm beforeAll(async () => { await dbHandler.connect() + const { user } = await dbHandler.insertFormCollectionReqs() + const bounceDoc = await new Bounce({ formId: new ObjectId(), bounces: [], @@ -62,18 +85,39 @@ describe('handleSns', () => { bounceDoc.areAllPermanentBounces = jest.fn() bounceDoc.hasNotified = jest.fn() mockBounceDoc = bounceDoc as IMockBounce + + mockForm = (await new FormModel({ + _id: bounceDoc.formId, + admin: user._id, + title: 'mockTitle', + }) + .populate('admin') + .execPopulate()) as IPopulatedForm }) afterAll(async () => await dbHandler.closeDatabase()) beforeEach(() => { - jest.clearAllMocks() + jest.resetAllMocks() + // Default mocks + MockBounceService.isValidSnsRequest.mockResolvedValue(true) + MockBounceService.extractEmailType.mockReturnValue(EmailType.AdminResponse) + MockBounceService.getUpdatedBounceDoc.mockResolvedValue(mockBounceDoc) + MockFormService.retrieveFullFormById.mockResolvedValue(okAsync(mockForm)) + mockBounceDoc.isCriticalBounce.mockReturnValue(true) + MockBounceService.getEditorsWithContactNumbers.mockResolvedValue( + MOCK_CONTACTS, + ) + mockBounceDoc.hasNotified.mockReturnValue(false) + MockBounceService.notifyAdminsOfBounce.mockResolvedValue({ + emailRecipients: MOCK_EMAIL_RECIPIENTS, + smsRecipients: MOCK_CONTACTS, + }) + // Note that this is true to simulate permanent bounce + mockBounceDoc.areAllPermanentBounces.mockReturnValue(true) }) - afterEach(async () => { - await dbHandler.clearDatabase() - jest.restoreAllMocks() - }) + afterEach(async () => await dbHandler.clearDatabase()) it('should return immediately when requests are invalid', async () => { MockBounceService.isValidSnsRequest.mockResolvedValueOnce(false) @@ -82,7 +126,13 @@ describe('handleSns', () => { MOCK_REQ.body, ) expect(MockBounceService.logEmailNotification).not.toHaveBeenCalled() - expect(MockBounceService.notifyAdminOfBounce).not.toHaveBeenCalled() + expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect( + MockBounceService.getEditorsWithContactNumbers, + ).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfBounce).not.toHaveBeenCalled() + expect(MockFormService.deactivateForm).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfDeactivation).not.toHaveBeenCalled() expect(MockBounceService.logCriticalBounce).not.toHaveBeenCalled() expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(403) }) @@ -96,24 +146,20 @@ describe('handleSns', () => { MOCK_REQ.body, ) expect(MockBounceService.logEmailNotification).not.toHaveBeenCalled() - expect(MockBounceService.notifyAdminOfBounce).not.toHaveBeenCalled() + expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect( + MockBounceService.getEditorsWithContactNumbers, + ).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfBounce).not.toHaveBeenCalled() + expect(MockFormService.deactivateForm).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfDeactivation).not.toHaveBeenCalled() expect(MockBounceService.logCriticalBounce).not.toHaveBeenCalled() expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(400) }) it('should call services correctly for permanent critical bounces', async () => { - MockBounceService.isValidSnsRequest.mockResolvedValueOnce(true) - MockBounceService.extractEmailType.mockReturnValueOnce( - EmailType.AdminResponse, - ) - MockBounceService.getUpdatedBounceDoc.mockResolvedValueOnce(mockBounceDoc) - mockBounceDoc.areAllPermanentBounces.mockReturnValueOnce(true) - mockBounceDoc.isCriticalBounce.mockReturnValueOnce(true) - mockBounceDoc.hasNotified.mockReturnValueOnce(false) - MockBounceService.notifyAdminOfBounce.mockResolvedValueOnce( - MOCK_EMAIL_RECIPIENTS, - ) - + // Note that default mocks simulate permanent critical bounce, so no changes + // to mocks are needed await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( @@ -128,31 +174,91 @@ describe('handleSns', () => { expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( MOCK_NOTIFICATION, ) - expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() - expect(MockFormService.deactivateForm).toHaveBeenCalledWith( + expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( mockBounceDoc.formId, ) - expect(MockBounceService.notifyAdminOfBounce).toHaveBeenCalledWith( + expect(mockBounceDoc.isCriticalBounce).toHaveBeenCalled() + expect(MockBounceService.getEditorsWithContactNumbers).toHaveBeenCalledWith( + mockForm, + ) + expect(MockBounceService.notifyAdminsOfBounce).toHaveBeenCalledWith( mockBounceDoc, + mockForm, + MOCK_CONTACTS, ) expect(mockBounceDoc.setNotificationState).toHaveBeenCalledWith( MOCK_EMAIL_RECIPIENTS, + MOCK_CONTACTS, ) - expect(MockBounceService.logCriticalBounce).toHaveBeenCalledWith( - mockBounceDoc, + expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() + expect(MockFormService.deactivateForm).toHaveBeenCalledWith( + mockBounceDoc.formId, + ) + expect(MockBounceService.notifyAdminsOfDeactivation).toHaveBeenCalledWith( + mockForm, + MOCK_CONTACTS, + ) + expect(MockBounceService.logCriticalBounce).toHaveBeenCalledWith({ + bounceDoc: mockBounceDoc, + notification: MOCK_NOTIFICATION, + autoEmailRecipients: MOCK_EMAIL_RECIPIENTS, + autoSmsRecipients: MOCK_CONTACTS, + hasDeactivated: true, + }) + expect(mockBounceDoc.save).toHaveBeenCalled() + expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(200) + }) + + it('should call services correctly for transient critical bounces', async () => { + // Note that this is false to simulate transient bounce + mockBounceDoc.areAllPermanentBounces.mockReturnValueOnce(false) + + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) + + expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( + MOCK_REQ.body, + ) + expect(MockBounceService.logEmailNotification).toHaveBeenCalledWith( + MOCK_NOTIFICATION, + ) + expect(MockBounceService.extractEmailType).toHaveBeenCalledWith( + MOCK_NOTIFICATION, + ) + expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( MOCK_NOTIFICATION, + ) + expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( + mockBounceDoc.formId, + ) + expect(mockBounceDoc.isCriticalBounce).toHaveBeenCalled() + expect(MockBounceService.getEditorsWithContactNumbers).toHaveBeenCalledWith( + mockForm, + ) + expect(MockBounceService.notifyAdminsOfBounce).toHaveBeenCalledWith( + mockBounceDoc, + mockForm, + MOCK_CONTACTS, + ) + expect(mockBounceDoc.setNotificationState).toHaveBeenCalledWith( MOCK_EMAIL_RECIPIENTS, - true, + MOCK_CONTACTS, ) + expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() + // Deactivation functions are not called + expect(MockFormService.deactivateForm).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfDeactivation).not.toHaveBeenCalled() + expect(MockBounceService.logCriticalBounce).toHaveBeenCalledWith({ + bounceDoc: mockBounceDoc, + notification: MOCK_NOTIFICATION, + autoEmailRecipients: MOCK_EMAIL_RECIPIENTS, + autoSmsRecipients: MOCK_CONTACTS, + hasDeactivated: false, + }) expect(mockBounceDoc.save).toHaveBeenCalled() expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(200) }) it('should return 400 when errors are thrown in getUpdatedBounceDoc', async () => { - MockBounceService.isValidSnsRequest.mockResolvedValueOnce(true) - MockBounceService.extractEmailType.mockReturnValueOnce( - EmailType.AdminResponse, - ) MockBounceService.getUpdatedBounceDoc.mockImplementationOnce(() => { throw new Error() }) @@ -172,13 +278,37 @@ describe('handleSns', () => { expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(400) }) - it('should return 400 when errors are thrown in deactivateForm', async () => { - MockBounceService.isValidSnsRequest.mockResolvedValueOnce(true) - MockBounceService.extractEmailType.mockReturnValueOnce( - EmailType.AdminResponse, + it('should return 500 early if error occurs while retrieving form', async () => { + MockFormService.retrieveFullFormById.mockResolvedValueOnce( + errAsync(new DatabaseError()), + ) + + await handleSns(MOCK_REQ, MOCK_RES, jest.fn()) + + expect(MockBounceService.isValidSnsRequest).toHaveBeenCalledWith( + MOCK_REQ.body, + ) + expect(MockBounceService.logEmailNotification).toHaveBeenCalledWith( + MOCK_NOTIFICATION, + ) + expect(MockBounceService.extractEmailType).toHaveBeenCalledWith( + MOCK_NOTIFICATION, + ) + expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( + MOCK_NOTIFICATION, + ) + expect(MockFormService.retrieveFullFormById).toHaveBeenLastCalledWith( + mockBounceDoc.formId, ) - MockBounceService.getUpdatedBounceDoc.mockResolvedValueOnce(mockBounceDoc) - mockBounceDoc.areAllPermanentBounces.mockReturnValueOnce(true) + expect( + MockBounceService.getEditorsWithContactNumbers, + ).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfBounce).not.toHaveBeenCalled() + expect(MockBounceService.notifyAdminsOfDeactivation).not.toHaveBeenCalled() + expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(500) + }) + + it('should return 400 when errors are thrown in deactivateForm', async () => { MockFormService.deactivateForm.mockImplementationOnce(() => { throw new Error() }) @@ -197,6 +327,13 @@ describe('handleSns', () => { expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( MOCK_NOTIFICATION, ) + expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( + mockBounceDoc.formId, + ) + expect(mockBounceDoc.isCriticalBounce).toHaveBeenCalled() + expect(MockBounceService.getEditorsWithContactNumbers).toHaveBeenCalledWith( + mockForm, + ) expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() expect(MockFormService.deactivateForm).toHaveBeenCalledWith( mockBounceDoc.formId, @@ -204,16 +341,8 @@ describe('handleSns', () => { expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(400) }) - it('should return 400 when errors are thrown in notifyAdminOfBounce', async () => { - MockBounceService.isValidSnsRequest.mockResolvedValueOnce(true) - MockBounceService.extractEmailType.mockReturnValueOnce( - EmailType.AdminResponse, - ) - MockBounceService.getUpdatedBounceDoc.mockResolvedValueOnce(mockBounceDoc) - mockBounceDoc.areAllPermanentBounces.mockReturnValueOnce(true) - mockBounceDoc.isCriticalBounce.mockReturnValueOnce(true) - mockBounceDoc.hasNotified.mockReturnValueOnce(false) - MockBounceService.notifyAdminOfBounce.mockImplementationOnce(() => { + it('should return 400 when errors are thrown in notifyAdminsOfBounce', async () => { + MockBounceService.notifyAdminsOfBounce.mockImplementationOnce(() => { throw new Error() }) @@ -231,28 +360,22 @@ describe('handleSns', () => { expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( MOCK_NOTIFICATION, ) - expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() - expect(MockFormService.deactivateForm).toHaveBeenCalledWith( + expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( mockBounceDoc.formId, ) - expect(MockBounceService.notifyAdminOfBounce).toHaveBeenCalledWith( + expect(mockBounceDoc.isCriticalBounce).toHaveBeenCalled() + expect(MockBounceService.getEditorsWithContactNumbers).toHaveBeenCalledWith( + mockForm, + ) + expect(MockBounceService.notifyAdminsOfBounce).toHaveBeenCalledWith( mockBounceDoc, + mockForm, + MOCK_CONTACTS, ) expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(400) }) it('should return 200 when VersionErrors are thrown', async () => { - MockBounceService.isValidSnsRequest.mockResolvedValueOnce(true) - MockBounceService.extractEmailType.mockReturnValueOnce( - EmailType.AdminResponse, - ) - MockBounceService.getUpdatedBounceDoc.mockResolvedValueOnce(mockBounceDoc) - mockBounceDoc.areAllPermanentBounces.mockReturnValueOnce(true) - mockBounceDoc.isCriticalBounce.mockReturnValueOnce(true) - mockBounceDoc.hasNotified.mockReturnValueOnce(false) - MockBounceService.notifyAdminOfBounce.mockResolvedValue( - MOCK_EMAIL_RECIPIENTS, - ) mockBounceDoc.save.mockImplementationOnce(() => { throw new MockVersionError() }) @@ -271,22 +394,34 @@ describe('handleSns', () => { expect(MockBounceService.getUpdatedBounceDoc).toHaveBeenCalledWith( MOCK_NOTIFICATION, ) + expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( + mockBounceDoc.formId, + ) + expect(mockBounceDoc.isCriticalBounce).toHaveBeenCalled() + expect(MockBounceService.getEditorsWithContactNumbers).toHaveBeenCalledWith( + mockForm, + ) expect(mockBounceDoc.areAllPermanentBounces).toHaveBeenCalled() expect(MockFormService.deactivateForm).toHaveBeenCalledWith( mockBounceDoc.formId, ) - expect(MockBounceService.notifyAdminOfBounce).toHaveBeenCalledWith( + expect(MockBounceService.notifyAdminsOfBounce).toHaveBeenCalledWith( mockBounceDoc, + mockForm, + MOCK_CONTACTS, ) expect(mockBounceDoc.setNotificationState).toHaveBeenCalledWith( MOCK_EMAIL_RECIPIENTS, - ) - expect(MockBounceService.logCriticalBounce).toHaveBeenCalledWith( - mockBounceDoc, - MOCK_NOTIFICATION, - MOCK_EMAIL_RECIPIENTS, - true, - ) + MOCK_CONTACTS, + ) + expect(MockBounceService.logCriticalBounce).toHaveBeenCalledWith({ + bounceDoc: mockBounceDoc, + notification: MOCK_NOTIFICATION, + autoEmailRecipients: MOCK_EMAIL_RECIPIENTS, + autoSmsRecipients: MOCK_CONTACTS, + hasDeactivated: true, + }) + expect(mockBounceDoc.save).toHaveBeenCalled() expect(MOCK_RES.sendStatus).toHaveBeenCalledWith(200) }) }) diff --git a/src/app/modules/bounce/__tests__/bounce.model.spec.ts b/src/app/modules/bounce/__tests__/bounce.model.spec.ts index 89cc6841d9..ca99883323 100644 --- a/src/app/modules/bounce/__tests__/bounce.model.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.model.spec.ts @@ -18,6 +18,11 @@ const Bounce = getBounceModel(mongoose) const MOCK_EMAIL = 'email@email.com' const MOCK_EMAIL_2 = 'email2@email.com' +const MOCK_CONTACT_1 = { + email: MOCK_EMAIL, + contact: '+6581234567', +} + describe('Bounce Model', () => { beforeAll(async () => await dbHandler.connect()) afterEach(async () => await dbHandler.clearDatabase()) @@ -35,6 +40,7 @@ describe('Bounce Model', () => { formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], hasAutoEmailed: false, + hasAutoSmsed: false, }) }) @@ -46,6 +52,7 @@ describe('Bounce Model', () => { ], expireAt: new Date(Date.now()), hasAutoEmailed: true, + hasAutoSmsed: true, } const savedBounce = await new Bounce(params).save() const savedBounceObject = extractBounceObject(savedBounce) @@ -68,7 +75,7 @@ describe('Bounce Model', () => { describe('methods', () => { describe('hasNotified', () => { - it('should return hasAutoEmailed if it is true', () => { + it('should return true if hasAutoEmailed is true', () => { const bounce = new Bounce({ formId: new ObjectId(), bounces: [], @@ -77,24 +84,74 @@ describe('Bounce Model', () => { expect(bounce.hasNotified()).toBe(true) }) - it('should return hasAutoEmailed if it is false', () => { + it('should return true if hasAutoSmsed is true', () => { + const bounce = new Bounce({ + formId: new ObjectId(), + bounces: [], + hasAutoSmsed: true, + }) + expect(bounce.hasNotified()).toBe(true) + }) + + it('should return false if both hasAutoEmailed and hasAutoSmsed are false', () => { const bounce = new Bounce({ formId: new ObjectId(), bounces: [], hasAutoEmailed: false, + hasAutoSmsed: false, }) expect(bounce.hasNotified()).toBe(false) }) }) describe('setNotificationState', () => { + it('should set hasAutoSmsed from false to true when there are SMS recipients', () => { + const bounce = new Bounce({ + formId: new ObjectId(), + bounces: [], + hasAutoSmsed: false, + }) + bounce.setNotificationState([], [MOCK_CONTACT_1]) + expect(bounce.hasAutoSmsed).toBe(true) + }) + + it('should keep hasAutoSmsed as true when there are SMS recipients', () => { + const bounce = new Bounce({ + formId: new ObjectId(), + bounces: [], + hasAutoSmsed: true, + }) + bounce.setNotificationState([], [MOCK_CONTACT_1]) + expect(bounce.hasAutoSmsed).toBe(true) + }) + + it('should keep original hasAutoSmsed as true when there are no SMS recipients', () => { + const bounce = new Bounce({ + formId: new ObjectId(), + bounces: [], + hasAutoSmsed: true, + }) + bounce.setNotificationState([], []) + expect(bounce.hasAutoSmsed).toBe(true) + }) + + it('should keep original hasAutoSmsed as false when there are no SMS recipients', () => { + const bounce = new Bounce({ + formId: new ObjectId(), + bounces: [], + hasAutoEmailed: false, + }) + bounce.setNotificationState([], []) + expect(bounce.hasAutoEmailed).toBe(false) + }) + it('should set hasAutoEmailed from false to true when there are email recipients', () => { const bounce = new Bounce({ formId: new ObjectId(), bounces: [], hasAutoEmailed: false, }) - bounce.setNotificationState([MOCK_EMAIL]) + bounce.setNotificationState([MOCK_EMAIL], []) expect(bounce.hasAutoEmailed).toBe(true) }) @@ -104,7 +161,7 @@ describe('Bounce Model', () => { bounces: [], hasAutoEmailed: true, }) - bounce.setNotificationState([MOCK_EMAIL]) + bounce.setNotificationState([MOCK_EMAIL], []) expect(bounce.hasAutoEmailed).toBe(true) }) @@ -114,7 +171,7 @@ describe('Bounce Model', () => { bounces: [], hasAutoEmailed: true, }) - bounce.setNotificationState([]) + bounce.setNotificationState([], []) expect(bounce.hasAutoEmailed).toBe(true) }) @@ -124,7 +181,7 @@ describe('Bounce Model', () => { bounces: [], hasAutoEmailed: false, }) - bounce.setNotificationState([]) + bounce.setNotificationState([], []) expect(bounce.hasAutoEmailed).toBe(false) }) }) @@ -542,6 +599,7 @@ describe('Bounce Model', () => { formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], hasAutoEmailed: false, + hasAutoSmsed: false, }) expect(actual!.expireAt).toBeInstanceOf(Date) }) @@ -564,6 +622,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Transient' }, ], hasAutoEmailed: false, + hasAutoSmsed: false, }) expect(actual!.expireAt).toBeInstanceOf(Date) }) @@ -586,6 +645,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, ], hasAutoEmailed: false, + hasAutoSmsed: false, }) expect(actual!.expireAt).toBeInstanceOf(Date) }) @@ -609,6 +669,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL_2, hasBounced: false }, ], hasAutoEmailed: false, + hasAutoSmsed: false, }) expect(actual!.expireAt).toBeInstanceOf(Date) }) diff --git a/src/app/modules/bounce/__tests__/bounce.service.spec.ts b/src/app/modules/bounce/__tests__/bounce.service.spec.ts index f899dd8b22..9f6bb34bf5 100644 --- a/src/app/modules/bounce/__tests__/bounce.service.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.service.spec.ts @@ -1,20 +1,32 @@ +/* eslint-disable import/first */ import axios from 'axios' import { ObjectId } from 'bson' import crypto from 'crypto' import dedent from 'dedent' import { cloneDeep, omit, pick } from 'lodash' import mongoose from 'mongoose' +import { errAsync, okAsync } from 'neverthrow' import { mocked } from 'ts-jest/utils' import getFormModel from 'src/app/models/form.server.model' +import * as UserService from 'src/app/modules/user/user.service' import { EMAIL_HEADERS, EmailType } from 'src/app/services/mail/mail.constants' import MailService from 'src/app/services/mail/mail.service' +import { SmsFactory } from 'src/app/services/sms/sms.factory' import * as LoggerModule from 'src/config/logger' -import { BounceType, ISnsNotification, IUserSchema } from 'src/types' +import { + BounceType, + IPopulatedForm, + ISnsNotification, + IUserSchema, +} from 'src/types' import dbHandler from 'tests/unit/backend/helpers/jest-db' import getMockLogger from 'tests/unit/backend/helpers/jest-logger' +import { DatabaseError } from '../../core/core.errors' +import { UserWithContactNumber } from '../bounce.types' + import { makeBounceNotification, MOCK_SNS_BODY } from './bounce-test-helpers' jest.mock('axios') @@ -23,6 +35,15 @@ jest.mock('src/config/logger') const MockLoggerModule = mocked(LoggerModule, true) jest.mock('src/app/services/mail/mail.service') const MockMailService = mocked(MailService, true) +jest.mock('src/app/services/sms/sms.factory', () => ({ + SmsFactory: { + sendFormDeactivatedSms: jest.fn(), + sendBouncedSubmissionSms: jest.fn(), + }, +})) +const MockSmsFactory = mocked(SmsFactory, true) +jest.mock('src/app/modules/user/user.service') +const MockUserService = mocked(UserService, true) const mockShortTermLogger = getMockLogger() const mockLogger = getMockLogger() @@ -30,16 +51,16 @@ MockLoggerModule.createCloudWatchLogger.mockReturnValue(mockShortTermLogger) MockLoggerModule.createLoggerWithLabel.mockReturnValue(mockLogger) // Import modules which depend on config last so that mocks get imported correctly -// eslint-disable-next-line import/first import getBounceModel from 'src/app/modules/bounce/bounce.model' -// eslint-disable-next-line import/first import { extractEmailType, + getEditorsWithContactNumbers, getUpdatedBounceDoc, isValidSnsRequest, logCriticalBounce, logEmailNotification, - notifyAdminOfBounce, + notifyAdminsOfBounce, + notifyAdminsOfDeactivation, } from 'src/app/modules/bounce/bounce.service' const Form = getFormModel(mongoose) @@ -47,6 +68,14 @@ const Bounce = getBounceModel(mongoose) const MOCK_EMAIL = 'email@example.com' const MOCK_EMAIL_2 = 'email2@example.com' +const MOCK_CONTACT = { + email: MOCK_EMAIL, + contact: '+6581234567', +} +const MOCK_CONTACT_2 = { + email: MOCK_EMAIL_2, + contact: '+6581234568', +} const MOCK_FORM_ID = new ObjectId() const MOCK_ADMIN_ID = new ObjectId() const MOCK_SUBMISSION_ID = new ObjectId() @@ -54,11 +83,13 @@ const MOCK_SUBMISSION_ID = new ObjectId() describe('BounceService', () => { beforeAll(async () => await dbHandler.connect()) - afterAll(async () => { + afterEach(async () => { await dbHandler.clearDatabase() - await dbHandler.closeDatabase() + jest.clearAllMocks() }) + afterAll(async () => await dbHandler.closeDatabase()) + describe('extractEmailType', () => { it('should extract the email type correctly', () => { const notification = makeBounceNotification({ @@ -236,108 +267,198 @@ describe('BounceService', () => { }) }) - describe('notifyAdminOfBounce', () => { + describe('notifyAdminsOfBounce', () => { const MOCK_FORM_TITLE = 'FormTitle' let testUser: IUserSchema - beforeAll(async () => { + beforeEach(async () => { const { user } = await dbHandler.insertFormCollectionReqs({ userId: MOCK_ADMIN_ID, }) testUser = user }) - afterAll(async () => { - await dbHandler.clearDatabase() - }) - beforeEach(async () => { jest.resetAllMocks() }) it('should auto-email when admin is not email recipient', async () => { - const form = new Form({ - admin: MOCK_ADMIN_ID, + const form = (await new Form({ + admin: testUser._id, title: MOCK_FORM_TITLE, }) - const testForm = await form.save() + .populate('admin') + .execPopulate()) as IPopulatedForm const bounceDoc = new Bounce({ - formId: testForm._id, + formId: form._id, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, ], }) - const emailRecipients = await notifyAdminOfBounce(bounceDoc) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, []) + expect(MockMailService.sendBounceNotification).toHaveBeenCalledWith({ emailRecipients: [testUser.email], bouncedRecipients: [MOCK_EMAIL], bounceType: BounceType.Permanent, - formTitle: testForm.title, - formId: testForm._id, + formTitle: form.title, + formId: form._id, }) - expect(emailRecipients).toEqual([testUser.email]) + expect(notifiedRecipients.emailRecipients).toEqual([testUser.email]) }) it('should auto-email when any collaborator is not email recipient', async () => { - const testForm = new Form({ - admin: MOCK_ADMIN_ID, + const collabEmail = 'collaborator@test.gov.sg' + const form = (await new Form({ + admin: testUser._id, title: MOCK_FORM_TITLE, + permissionList: [{ email: collabEmail, write: true }], }) - const collabEmail = 'collaborator@test.gov.sg' - testForm.permissionList = [{ email: collabEmail, write: true }] - await testForm.save() + .populate('admin') + .execPopulate()) as IPopulatedForm const bounceDoc = new Bounce({ - formId: testForm._id, + formId: form._id, bounces: [ { email: testUser.email, hasBounced: true, bounceType: 'Permanent' }, ], }) - const emailRecipients = await notifyAdminOfBounce(bounceDoc) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, []) + expect(MockMailService.sendBounceNotification).toHaveBeenCalledWith({ emailRecipients: [collabEmail], bouncedRecipients: [testUser.email], bounceType: BounceType.Permanent, - formTitle: testForm.title, - formId: testForm._id, + formTitle: form.title, + formId: form._id, }) - expect(emailRecipients).toEqual([collabEmail]) + expect(notifiedRecipients.emailRecipients).toEqual([collabEmail]) }) it('should not auto-email when admin is email recipient', async () => { - const testForm = new Form({ - admin: MOCK_ADMIN_ID, + const form = (await new Form({ + admin: testUser._id, title: MOCK_FORM_TITLE, }) - await testForm.save() + .populate('admin') + .execPopulate()) as IPopulatedForm const bounceDoc = new Bounce({ - formId: testForm._id, + formId: form._id, bounces: [ { email: testUser.email, hasBounced: true, bounceType: 'Permanent' }, ], }) - const emailRecipients = await notifyAdminOfBounce(bounceDoc) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, []) + expect(MockMailService.sendBounceNotification).not.toHaveBeenCalled() - expect(emailRecipients).toEqual([]) + expect(notifiedRecipients.emailRecipients).toEqual([]) }) it('should not auto-email when all collabs are email recipients', async () => { - const testForm = new Form({ - admin: MOCK_ADMIN_ID, + const collabEmail = 'collaborator@test.gov.sg' + const form = (await new Form({ + admin: testUser._id, title: MOCK_FORM_TITLE, + permissionList: [{ email: collabEmail, write: false }], }) - const collabEmail = 'collaborator@test.gov.sg' - testForm.permissionList = [{ email: collabEmail, write: false }] - await testForm.save() + .populate('admin') + .execPopulate()) as IPopulatedForm const bounceDoc = new Bounce({ - formId: testForm._id, + formId: form._id, bounces: [ { email: testUser.email, hasBounced: true, bounceType: 'Permanent' }, { email: collabEmail, hasBounced: true, bounceType: 'Permanent' }, ], }) - const emailRecipients = await notifyAdminOfBounce(bounceDoc) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, []) + expect(MockMailService.sendBounceNotification).not.toHaveBeenCalled() - expect(emailRecipients).toEqual([]) + expect(notifiedRecipients.emailRecipients).toEqual([]) + }) + + it('should send text for all SMS recipients and return successful ones', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + const bounceDoc = new Bounce({ + formId: form._id, + bounces: [], + }) + MockSmsFactory.sendBouncedSubmissionSms.mockResolvedValue(true) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, [ + MOCK_CONTACT, + MOCK_CONTACT_2, + ]) + + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledTimes(2) + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ + adminEmail: testUser.email, + adminId: testUser._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT.contact, + recipientEmail: MOCK_CONTACT.email, + }) + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ + adminEmail: testUser.email, + adminId: testUser._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT_2.contact, + recipientEmail: MOCK_CONTACT_2.email, + }) + expect(notifiedRecipients.smsRecipients).toEqual([ + MOCK_CONTACT, + MOCK_CONTACT_2, + ]) + }) + + it('should return only successfuly SMS recipients when some SMSes fail', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + const bounceDoc = new Bounce({ + formId: form._id, + bounces: [], + }) + const mockRejectedReason = 'reasons' + MockSmsFactory.sendBouncedSubmissionSms + .mockResolvedValueOnce(true) + .mockRejectedValueOnce(mockRejectedReason) + + const notifiedRecipients = await notifyAdminsOfBounce(bounceDoc, form, [ + MOCK_CONTACT, + MOCK_CONTACT_2, + ]) + + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledTimes(2) + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ + adminEmail: testUser.email, + adminId: testUser._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT.contact, + recipientEmail: MOCK_CONTACT.email, + }) + expect(MockSmsFactory.sendBouncedSubmissionSms).toHaveBeenCalledWith({ + adminEmail: testUser.email, + adminId: testUser._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT_2.contact, + recipientEmail: MOCK_CONTACT_2.email, + }) + expect(notifiedRecipients.smsRecipients).toEqual([MOCK_CONTACT]) }) }) @@ -354,6 +475,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL_2, hasBounced: true, bounceType: 'Transient' }, ], hasAutoEmailed: true, + hasAutoSmsed: true, }) const snsInfo = makeBounceNotification({ formId: MOCK_FORM_ID, @@ -362,12 +484,20 @@ describe('BounceService', () => { bouncedList: [MOCK_EMAIL], }) const autoEmailRecipients = [MOCK_EMAIL, MOCK_EMAIL_2] - logCriticalBounce(bounceDoc, snsInfo, autoEmailRecipients, false) + const autoSmsRecipients = [MOCK_CONTACT, MOCK_CONTACT_2] + logCriticalBounce({ + bounceDoc, + notification: snsInfo, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated: false, + }) expect(mockLogger.warn).toHaveBeenCalledWith({ message: 'Bounced submission', meta: { action: 'logCriticalBounce', hasAutoEmailed: true, + hasAutoSmsed: true, hasDeactivated: false, formId: String(MOCK_FORM_ID), submissionId: String(MOCK_SUBMISSION_ID), @@ -376,6 +506,7 @@ describe('BounceService', () => { numTransient: 2, numPermanent: 0, autoEmailRecipients, + autoSmsRecipients, bounceInfo: snsInfo.bounce, }, }) @@ -389,6 +520,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL_2, hasBounced: true, bounceType: 'Permanent' }, ], hasAutoEmailed: true, + hasAutoSmsed: true, }) const snsInfo = makeBounceNotification({ formId: MOCK_FORM_ID, @@ -397,12 +529,20 @@ describe('BounceService', () => { bouncedList: [MOCK_EMAIL], }) const autoEmailRecipients: string[] = [] - logCriticalBounce(bounceDoc, snsInfo, autoEmailRecipients, true) + const autoSmsRecipients = [MOCK_CONTACT, MOCK_CONTACT_2] + logCriticalBounce({ + bounceDoc, + notification: snsInfo, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated: true, + }) expect(mockLogger.warn).toHaveBeenCalledWith({ message: 'Bounced submission', meta: { action: 'logCriticalBounce', hasAutoEmailed: true, + hasAutoSmsed: true, hasDeactivated: true, formId: String(MOCK_FORM_ID), submissionId: String(MOCK_SUBMISSION_ID), @@ -411,6 +551,7 @@ describe('BounceService', () => { numTransient: 0, numPermanent: 2, autoEmailRecipients, + autoSmsRecipients, bounceInfo: snsInfo.bounce, }, }) @@ -424,6 +565,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL_2, hasBounced: true, bounceType: 'Transient' }, ], hasAutoEmailed: true, + hasAutoSmsed: true, }) const snsInfo = makeBounceNotification({ formId: MOCK_FORM_ID, @@ -432,12 +574,20 @@ describe('BounceService', () => { bouncedList: [MOCK_EMAIL], }) const autoEmailRecipients: string[] = [] - logCriticalBounce(bounceDoc, snsInfo, autoEmailRecipients, false) + const autoSmsRecipients = [MOCK_CONTACT, MOCK_CONTACT_2] + logCriticalBounce({ + bounceDoc, + notification: snsInfo, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated: false, + }) expect(mockLogger.warn).toHaveBeenCalledWith({ message: 'Bounced submission', meta: { action: 'logCriticalBounce', hasAutoEmailed: true, + hasAutoSmsed: true, hasDeactivated: false, formId: String(MOCK_FORM_ID), submissionId: String(MOCK_SUBMISSION_ID), @@ -446,6 +596,91 @@ describe('BounceService', () => { numTransient: 1, numPermanent: 1, autoEmailRecipients, + autoSmsRecipients, + bounceInfo: snsInfo.bounce, + }, + }) + }) + + it('should log correctly when hasAutoEmailed is false', () => { + const bounceDoc = new Bounce({ + formId: MOCK_FORM_ID, + bounces: [], + hasAutoEmailed: false, + hasAutoSmsed: true, + }) + const snsInfo = makeBounceNotification({ + formId: MOCK_FORM_ID, + submissionId: MOCK_SUBMISSION_ID, + recipientList: [MOCK_EMAIL, MOCK_EMAIL_2], + bouncedList: [], + }) + const autoEmailRecipients: string[] = [] + const autoSmsRecipients: UserWithContactNumber[] = [] + logCriticalBounce({ + bounceDoc, + notification: snsInfo, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated: false, + }) + expect(mockLogger.warn).toHaveBeenCalledWith({ + message: 'Bounced submission', + meta: { + action: 'logCriticalBounce', + hasAutoEmailed: false, + hasAutoSmsed: true, + hasDeactivated: false, + formId: String(MOCK_FORM_ID), + submissionId: String(MOCK_SUBMISSION_ID), + recipients: [], + numRecipients: 0, + numTransient: 0, + numPermanent: 0, + autoEmailRecipients, + autoSmsRecipients, + bounceInfo: snsInfo.bounce, + }, + }) + }) + + it('should log correctly when hasAutoSmsed is false', () => { + const bounceDoc = new Bounce({ + formId: MOCK_FORM_ID, + bounces: [], + hasAutoEmailed: true, + hasAutoSmsed: false, + }) + const snsInfo = makeBounceNotification({ + formId: MOCK_FORM_ID, + submissionId: MOCK_SUBMISSION_ID, + recipientList: [MOCK_EMAIL, MOCK_EMAIL_2], + bouncedList: [], + }) + const autoEmailRecipients: string[] = [] + const autoSmsRecipients: UserWithContactNumber[] = [] + logCriticalBounce({ + bounceDoc, + notification: snsInfo, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated: false, + }) + expect(mockLogger.warn).toHaveBeenCalledWith({ + message: 'Bounced submission', + meta: { + action: 'logCriticalBounce', + hasAutoEmailed: true, + hasAutoSmsed: false, + hasDeactivated: false, + formId: String(MOCK_FORM_ID), + submissionId: String(MOCK_SUBMISSION_ID), + recipients: [], + numRecipients: 0, + numTransient: 0, + numPermanent: 0, + autoEmailRecipients, + autoSmsRecipients, bounceInfo: snsInfo.bounce, }, }) @@ -516,4 +751,178 @@ describe('BounceService', () => { return expect(isValidSnsRequest(body)).resolves.toBe(true) }) }) + + describe('getEditorsWithContactNumbers', () => { + const MOCK_FORM_TITLE = 'FormTitle' + let testUser: IUserSchema + + beforeEach(async () => { + const { user } = await dbHandler.insertFormCollectionReqs({ + userId: MOCK_ADMIN_ID, + }) + testUser = user + }) + + beforeEach(async () => { + jest.resetAllMocks() + }) + + it('should call user service with emails of all write collaborators', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + permissionList: [ + { email: MOCK_EMAIL, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ], + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + MockUserService.findContactsForEmails.mockResolvedValueOnce( + okAsync([MOCK_CONTACT]), + ) + + const result = await getEditorsWithContactNumbers(form) + + expect(MockUserService.findContactsForEmails).toHaveBeenCalledWith([ + form.admin.email, + MOCK_EMAIL, + ]) + expect(result).toEqual([MOCK_CONTACT]) + }) + + it('should filter out collaborators without contact numbers', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + permissionList: [ + { email: MOCK_EMAIL, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ], + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + MockUserService.findContactsForEmails.mockResolvedValueOnce( + okAsync([omit(MOCK_CONTACT, 'contact'), MOCK_CONTACT_2]), + ) + + const result = await getEditorsWithContactNumbers(form) + + expect(MockUserService.findContactsForEmails).toHaveBeenCalledWith([ + form.admin.email, + MOCK_EMAIL, + ]) + expect(result).toEqual([MOCK_CONTACT_2]) + }) + + it('should return empty array when UserService returns error', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + permissionList: [ + { email: MOCK_EMAIL, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ], + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + MockUserService.findContactsForEmails.mockResolvedValueOnce( + errAsync(new DatabaseError()), + ) + + const result = await getEditorsWithContactNumbers(form) + + expect(MockUserService.findContactsForEmails).toHaveBeenCalledWith([ + form.admin.email, + MOCK_EMAIL, + ]) + expect(result).toEqual([]) + }) + }) + + describe('notifyAdminsOfDeactivation', () => { + const MOCK_FORM_TITLE = 'FormTitle' + let testUser: IUserSchema + + beforeEach(async () => { + const { user } = await dbHandler.insertFormCollectionReqs({ + userId: MOCK_ADMIN_ID, + }) + testUser = user + }) + + beforeEach(async () => { + jest.resetAllMocks() + }) + + it('should send SMS for all given recipients', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + MockSmsFactory.sendFormDeactivatedSms.mockResolvedValue(true) + + const result = await notifyAdminsOfDeactivation(form, [ + MOCK_CONTACT, + MOCK_CONTACT_2, + ]) + + expect(result).toEqual(true) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledTimes(2) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT.contact, + recipientEmail: MOCK_CONTACT.email, + }) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT_2.contact, + recipientEmail: MOCK_CONTACT_2.email, + }) + }) + + it('should return true even when some SMSes fail', async () => { + const form = (await new Form({ + admin: testUser._id, + title: MOCK_FORM_TITLE, + }) + .populate('admin') + .execPopulate()) as IPopulatedForm + MockSmsFactory.sendFormDeactivatedSms + .mockResolvedValueOnce(true) + .mockRejectedValueOnce(new Error()) + + const result = await notifyAdminsOfDeactivation(form, [ + MOCK_CONTACT, + MOCK_CONTACT_2, + ]) + + expect(result).toEqual(true) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledTimes(2) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT.contact, + recipientEmail: MOCK_CONTACT.email, + }) + expect(MockSmsFactory.sendFormDeactivatedSms).toHaveBeenCalledWith({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: MOCK_CONTACT_2.contact, + recipientEmail: MOCK_CONTACT_2.email, + }) + }) + }) }) diff --git a/src/app/modules/bounce/bounce.controller.ts b/src/app/modules/bounce/bounce.controller.ts index 873c4b2033..160cbff392 100644 --- a/src/app/modules/bounce/bounce.controller.ts +++ b/src/app/modules/bounce/bounce.controller.ts @@ -9,11 +9,13 @@ import { EmailType } from '../../services/mail/mail.constants' import * as FormService from '../form/form.service' import * as BounceService from './bounce.service' +import { AdminNotificationResult } from './bounce.types' const logger = createLoggerWithLabel(module) /** * Validates that a request came from Amazon SNS, then updates the Bounce - * collection. + * collection. Also informs form admins and collaborators if their form responses + * bounced. * @param req Express request object * @param res - Express response object */ @@ -39,22 +41,63 @@ export const handleSns: RequestHandler< const bounceDoc = await BounceService.getUpdatedBounceDoc(notification) // Missing headers in notification if (!bounceDoc) return res.sendStatus(StatusCodes.OK) - const shouldDeactivate = bounceDoc.areAllPermanentBounces() - if (shouldDeactivate) { - await FormService.deactivateForm(bounceDoc.formId) + + const formResult = await FormService.retrieveFullFormById(bounceDoc.formId) + if (formResult.isErr()) { + // Either database error occurred or the formId saved in the bounce collection + // doesn't exist, so something went wrong. + logger.error({ + message: 'Failed to retrieve form corresponding to bounced formId', + meta: { + action: 'handleSns', + formId: bounceDoc.formId, + }, + }) + return res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR) } + const form = formResult.value + if (bounceDoc.isCriticalBounce()) { - let emailRecipients: string[] = [] + // Get contact numbers + const possibleSmsRecipients = await BounceService.getEditorsWithContactNumbers( + form, + ) + + // Notify admin and collaborators + let notificationRecipients: AdminNotificationResult = { + emailRecipients: [], + smsRecipients: [], + } if (!bounceDoc.hasNotified()) { - emailRecipients = await BounceService.notifyAdminOfBounce(bounceDoc) - bounceDoc.setNotificationState(emailRecipients) + notificationRecipients = await BounceService.notifyAdminsOfBounce( + bounceDoc, + form, + possibleSmsRecipients, + ) + bounceDoc.setNotificationState( + notificationRecipients.emailRecipients, + notificationRecipients.smsRecipients, + ) } - BounceService.logCriticalBounce( + + // Deactivate if all bounces are permanent + const shouldDeactivate = bounceDoc.areAllPermanentBounces() + if (shouldDeactivate) { + await FormService.deactivateForm(bounceDoc.formId) + await BounceService.notifyAdminsOfDeactivation( + form, + possibleSmsRecipients, + ) + } + + // Important log message for user follow-ups + BounceService.logCriticalBounce({ bounceDoc, notification, - emailRecipients, - shouldDeactivate, - ) + autoEmailRecipients: notificationRecipients.emailRecipients, + autoSmsRecipients: notificationRecipients.smsRecipients, + hasDeactivated: shouldDeactivate, + }) } await bounceDoc.save() return res.sendStatus(StatusCodes.OK) diff --git a/src/app/modules/bounce/bounce.model.ts b/src/app/modules/bounce/bounce.model.ts index b377ae878d..8f1d9934d0 100644 --- a/src/app/modules/bounce/bounce.model.ts +++ b/src/app/modules/bounce/bounce.model.ts @@ -8,6 +8,7 @@ import { IBounceSchema, IEmailNotification, ISingleBounce, + UserContactView, } from '../../../types' import { FORM_SCHEMA_ID } from '../../models/form.server.model' @@ -32,6 +33,10 @@ const BounceSchema = new Schema({ type: Boolean, default: false, }, + hasAutoSmsed: { + type: Boolean, + default: false, + }, bounces: { type: [ { @@ -95,6 +100,7 @@ BounceSchema.statics.fromSnsNotification = function ( /** * Updates an old bounce document with info from an SNS notification. * @param snsInfo the notification information to merge + * @returns the updated document */ BounceSchema.methods.updateBounceInfo = function ( this: IBounceSchema, @@ -133,6 +139,7 @@ BounceSchema.methods.updateBounceInfo = function ( /** * Returns true if the document indicates a critical bounce (all recipients * bounced), false otherwise. + * @returns true if all recipients bounced */ BounceSchema.methods.isCriticalBounce = function ( this: IBounceSchema, @@ -143,6 +150,7 @@ BounceSchema.methods.isCriticalBounce = function ( /** * Returns true if the document indicates that all recipients bounced and * all bounces were permanent, false otherwise. + * @returns true if all bounecs were permanent */ BounceSchema.methods.areAllPermanentBounces = function ( this: IBounceSchema, @@ -155,6 +163,7 @@ BounceSchema.methods.areAllPermanentBounces = function ( /** * Returns the list of email recipients for this form + * @returns Array of email addresses */ BounceSchema.methods.getEmails = function (this: IBounceSchema): string[] { // Return a regular array to prevent unexpected bugs with mongoose @@ -165,22 +174,28 @@ BounceSchema.methods.getEmails = function (this: IBounceSchema): string[] { /** * Sets hasAutoEmailed to true if at least one person has been emailed. * @param emailRecipients Array of recipients who were emailed. + * @returns void. Modifies document in place. */ BounceSchema.methods.setNotificationState = function ( this: IBounceSchema, emailRecipients: string[], + smsRecipients: UserContactView[], ): void { if (emailRecipients.length > 0) { this.hasAutoEmailed = true } + if (smsRecipients.length > 0) { + this.hasAutoSmsed = true + } } /** - * Returns true if an automated email has been sent for this form, + * Returns true if an automated email or SMS has been sent for this form, * false otherwise. + * @returns true if at least one admin or collaborator has been notified */ BounceSchema.methods.hasNotified = function (this: IBounceSchema): boolean { - return this.hasAutoEmailed + return this.hasAutoEmailed || this.hasAutoSmsed } const getBounceModel = (db: Mongoose): IBounceModel => { diff --git a/src/app/modules/bounce/bounce.service.ts b/src/app/modules/bounce/bounce.service.ts index c20b18a2bf..e543b84802 100644 --- a/src/app/modules/bounce/bounce.service.ts +++ b/src/app/modules/bounce/bounce.service.ts @@ -14,17 +14,24 @@ import { IPopulatedForm, ISnsNotification, } from '../../../types' -import getFormModel from '../../models/form.server.model' import { EMAIL_HEADERS, EmailType } from '../../services/mail/mail.constants' import MailService from '../../services/mail/mail.service' +import { SmsFactory } from '../../services/sms/sms.factory' +import { getCollabEmailsWithPermission } from '../form/form.utils' +import * as UserService from '../user/user.service' import getBounceModel from './bounce.model' -import { extractHeader, isBounceNotification } from './bounce.util' +import { AdminNotificationResult, UserWithContactNumber } from './bounce.types' +import { + extractHeader, + extractSmsErrors, + extractSuccessfulSmsRecipients, + isBounceNotification, +} from './bounce.util' const logger = createLoggerWithLabel(module) const shortTermLogger = createCloudWatchLogger('email') const Bounce = getBounceModel(mongoose) -const Form = getFormModel(mongoose) // Note that these need to be ordered in order to generate // the correct string to sign @@ -44,7 +51,8 @@ const AWS_HOSTNAME = '.amazonaws.com' /** * Checks that a request body has all the required keys for a message from SNS. - * @param {Object} body body from Express request object + * @param body body from Express request object + * @returns true if all required keys are present */ const hasRequiredKeys = (body: any): body is ISnsNotification => { return !isEmpty(body) && snsKeys.every((keyObj) => body[keyObj.key]) @@ -52,7 +60,8 @@ const hasRequiredKeys = (body: any): body is ISnsNotification => { /** * Validates that a URL points to a certificate belonging to AWS. - * @param {String} url URL to check + * @param url URL to check + * @returns true if URL is valid */ const isValidCertUrl = (certUrl: string): boolean => { const parsed = new URL(certUrl) @@ -65,6 +74,7 @@ const isValidCertUrl = (certUrl: string): boolean => { /** * Returns an ordered list of keys to include in SNS signing string. + * @returns array of keys */ const getSnsKeysToSign = (): (keyof ISnsNotification)[] => { return snsKeys.filter((keyObj) => keyObj.toSign).map((keyObj) => keyObj.key) @@ -72,7 +82,8 @@ const getSnsKeysToSign = (): (keyof ISnsNotification)[] => { /** * Generates the string to sign. - * @param {Object} body body from Express request object + * @param body body from Express request object + * @returns the basestring to be signed */ const getSnsBasestring = (body: ISnsNotification): string => { return getSnsKeysToSign().reduce((result, key) => { @@ -82,7 +93,8 @@ const getSnsBasestring = (body: ISnsNotification): string => { /** * Verify signature for SNS request - * @param {Object} body body from Express request object + * @param body body from Express request object + * @returns true if signature is valid */ const isValidSnsSignature = async ( body: ISnsNotification, @@ -96,7 +108,8 @@ const isValidSnsSignature = async ( /** * Verifies if a request object is correctly signed by Amazon SNS. More info: * https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html - * @param {Object} body Body of Express request object + * @param body Body of Express request object + * @returns true if request shape and signature are valid */ export const isValidSnsRequest = async ( body: ISnsNotification, @@ -109,13 +122,28 @@ export const isValidSnsRequest = async ( return isValid } -// Writes a log message if all recipients have bounced -export const logCriticalBounce = ( - bounceDoc: IBounceSchema, - notification: IEmailNotification, - autoEmailRecipients: string[], - hasDeactivated: boolean, -): void => { +/** + * Logs all details of lost submission. + * @param bounceDoc Document from the Bounce collection + * @param notification Notification received from SNS + * @param autoEmailRecipients Recipients who were emailed + * @param autoSmsRecipients Recipients who were SMSed + * @param hasDeactivated Whether the form was deactivated + * @returns void + */ +export const logCriticalBounce = ({ + bounceDoc, + notification, + autoEmailRecipients, + autoSmsRecipients, + hasDeactivated, +}: { + bounceDoc: IBounceSchema + notification: IEmailNotification + autoEmailRecipients: string[] + autoSmsRecipients: UserWithContactNumber[] + hasDeactivated: boolean +}): void => { const submissionId = extractHeader(notification, EMAIL_HEADERS.submissionId) const bounceInfo = isBounceNotification(notification) ? notification.bounce @@ -132,6 +160,7 @@ export const logCriticalBounce = ( meta: { action: 'logCriticalBounce', hasAutoEmailed: bounceDoc.hasAutoEmailed, + hasAutoSmsed: bounceDoc.hasAutoSmsed, hasDeactivated, formId: String(bounceDoc.formId), submissionId: submissionId, @@ -141,6 +170,7 @@ export const logCriticalBounce = ( // Assume that this function is correctly only called when all recipients bounced numPermanent: bounceDoc.bounces.length - numTransient, autoEmailRecipients, + autoSmsRecipients, // We know for sure that critical bounces can only happen because of bounce // notifications, so we don't expect this to be undefined bounceInfo: bounceInfo, @@ -148,31 +178,39 @@ export const logCriticalBounce = ( }) } +/** + * Gets the emails of form admin and collaborators who do not appear in the + * given Bounce document + * @param populatedForm Form whose admin and collaborators should be included + * @param bounceDoc Bounce document indicating which emails bounced + * @returns array of emails + */ const computeValidEmails = ( populatedForm: IPopulatedForm, bounceDoc: IBounceSchema, ): string[] => { - const collabEmails = populatedForm.permissionList - ? populatedForm.permissionList.map((collab) => collab.email) - : [] + const collabEmails = getCollabEmailsWithPermission( + populatedForm.permissionList, + ) const possibleEmails = collabEmails.concat(populatedForm.admin.email) return difference(possibleEmails, bounceDoc.getEmails()) } -export const notifyAdminOfBounce = async ( +/** + * Notifies admin and collaborators via email and SMS that response was lost. + * @param bounceDoc Document from Bounce collection + * @param form Form corresponding to the formId from bounceDoc + * @param possibleSmsRecipients Contact details of recipients to attempt to SMS + * @returns contact details for email and SMSes which were successfully sent. Note that + * this doesn't mean the emails and SMSes were received, only that they were delivered + * to the mail server/carrier. + */ +export const notifyAdminsOfBounce = async ( bounceDoc: IBounceSchema, -): Promise => { - const form = await Form.getFullFormById(bounceDoc.formId) - if (!form) { - logger.error({ - message: 'Unable to retrieve form', - meta: { - action: 'notifyAdminOfBounce', - formId: bounceDoc.formId, - }, - }) - return [] - } + form: IPopulatedForm, + possibleSmsRecipients: UserWithContactNumber[], +): Promise => { + // Email all collaborators const emailRecipients = computeValidEmails(form, bounceDoc) if (emailRecipients.length > 0) { await MailService.sendBounceNotification({ @@ -185,12 +223,40 @@ export const notifyAdminOfBounce = async ( formId: bounceDoc.formId, }) } - return emailRecipients + + // Sms given recipients + const smsPromises = possibleSmsRecipients.map((recipient) => + SmsFactory.sendBouncedSubmissionSms({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: recipient.contact, + recipientEmail: recipient.email, + }), + ) + const smsResults = await Promise.allSettled(smsPromises) + const successfulSmsRecipients = extractSuccessfulSmsRecipients( + smsResults, + possibleSmsRecipients, + ) + if (successfulSmsRecipients.length < possibleSmsRecipients.length) { + logger.warn({ + message: 'Failed to send some bounce notification SMSes', + meta: { + action: 'notifyAdminOfBounce', + formId: form._id, + reasons: extractSmsErrors(smsResults), + }, + }) + } + return { emailRecipients, smsRecipients: successfulSmsRecipients } } /** * Logs the raw notification to the relevant log group. * @param notification The parsed SNS notification + * @returns void */ export const logEmailNotification = ( notification: IEmailNotification, @@ -243,3 +309,72 @@ export const extractEmailType = ( ): string | undefined => { return extractHeader(notification, EMAIL_HEADERS.emailType) } + +/** + * Retrieves contact details for admin and collaborators for whom + * SMS notification should be attempted. + * @param form The form whose editors should be found + * @returns The contact details, filtered for the emails which have verified + * contact numbers in the database + */ +export const getEditorsWithContactNumbers = async ( + form: IPopulatedForm, +): Promise => { + const possibleEditors = [ + form.admin.email, + ...getCollabEmailsWithPermission(form.permissionList, true), + ] + const smsRecipientsResult = await UserService.findContactsForEmails( + possibleEditors, + ) + if (smsRecipientsResult.isOk()) { + return smsRecipientsResult.value.filter( + (r) => !!r.contact, + ) as UserWithContactNumber[] + } else { + logger.warn({ + message: 'Failed to retrieve contact numbers for form editors', + meta: { + action: 'getEditorsWithContactNumbers', + formId: form._id, + }, + }) + return [] + } +} + +/** + * Sends SMS to the given recipients informing them that the given form + * was deactivated. + * @param form Form which was deactivated + * @param possibleSmsRecipients Recipients to attempt to notify + * @returns true regardless of the outcome + */ +export const notifyAdminsOfDeactivation = async ( + form: IPopulatedForm, + possibleSmsRecipients: UserWithContactNumber[], +): Promise => { + const smsPromises = possibleSmsRecipients.map((recipient) => + SmsFactory.sendFormDeactivatedSms({ + adminEmail: form.admin.email, + adminId: form.admin._id, + formId: form._id, + formTitle: form.title, + recipient: recipient.contact, + recipientEmail: recipient.email, + }), + ) + const smsResults = await Promise.allSettled(smsPromises) + const smsErrors = extractSmsErrors(smsResults) + if (smsErrors.length > 0) { + logger.warn({ + message: 'Failed to send some form deactivation notification SMSes', + meta: { + action: 'notifyAdminsOfDeactivation', + formId: form._id, + reasons: smsErrors, + }, + }) + } + return true +} diff --git a/src/app/modules/bounce/bounce.types.ts b/src/app/modules/bounce/bounce.types.ts new file mode 100644 index 0000000000..0246f4c1e1 --- /dev/null +++ b/src/app/modules/bounce/bounce.types.ts @@ -0,0 +1,10 @@ +import { SetRequired } from 'type-fest' + +import { UserContactView } from '../../../types' + +export type UserWithContactNumber = SetRequired + +export interface AdminNotificationResult { + emailRecipients: string[] + smsRecipients: UserWithContactNumber[] +} diff --git a/src/app/modules/bounce/bounce.util.ts b/src/app/modules/bounce/bounce.util.ts index 278878f3a3..c690d5cb61 100644 --- a/src/app/modules/bounce/bounce.util.ts +++ b/src/app/modules/bounce/bounce.util.ts @@ -3,6 +3,8 @@ import { IDeliveryNotification, IEmailNotification, } from '../../../types' + +import { UserWithContactNumber } from './bounce.types' /** * Extracts custom headers which we send with all emails, such as form ID, submission ID * and email type (admin response, email confirmation OTP etc). @@ -62,3 +64,39 @@ export const isBounceNotification = ( export const isDeliveryNotification = ( body: IEmailNotification, ): body is IDeliveryNotification => body.notificationType === 'Delivery' + +/** + * Filters the given SMS recipients to only those which were sent succesfully + * @param smsResults Array of Promise.allSettled results + * @param smsRecipients Recipients who were SMSed. This array must correspond + * exactly to smsResults, i.e. the result at smsResults[i] corresponds + * to the result of the attempt to SMS smsRecipients[i] + * @returns the contact details of SMSes sent successfully + */ +export const extractSuccessfulSmsRecipients = ( + smsResults: PromiseSettledResult[], + smsRecipients: UserWithContactNumber[], +): UserWithContactNumber[] => { + return smsResults.reduce((acc, result, index) => { + if (result.status === 'fulfilled') { + acc.push(smsRecipients[index]) + } + return acc + }, []) +} + +/** + * Extracts the errors from results of attempting to send SMSes + * @param smsResults Array of Promise.allSettled results + * @returns Array of errors + */ +export const extractSmsErrors = ( + smsResults: PromiseSettledResult[], +): PromiseRejectedResult['reason'][] => { + return smsResults.reduce((acc, result) => { + if (result.status === 'rejected') { + acc.push(result.reason) + } + return acc + }, []) +} diff --git a/src/app/modules/form/__tests__/form.utils.spec.ts b/src/app/modules/form/__tests__/form.utils.spec.ts index 9f93e71f54..699aa89599 100644 --- a/src/app/modules/form/__tests__/form.utils.spec.ts +++ b/src/app/modules/form/__tests__/form.utils.spec.ts @@ -9,11 +9,18 @@ import { IFieldSchema, IPopulatedForm, IPopulatedUser, + Permission, ResponseMode, Status, } from 'src/types' -import { removePrivateDetailsFromForm } from '../form.utils' +import { + getCollabEmailsWithPermission, + removePrivateDetailsFromForm, +} from '../form.utils' + +const MOCK_EMAIL_1 = 'a@abc.com' +const MOCK_EMAIL_2 = 'b@def.com' const MOCK_POPULATED_FORM = ({ startPage: { @@ -75,6 +82,43 @@ const MOCK_POPULATED_FORM = ({ } as unknown) as IPopulatedForm describe('form.utils', () => { + describe('getCollabEmailsWithPermission', () => { + it('should return empty array when no arguments are given', () => { + expect(getCollabEmailsWithPermission()).toEqual([]) + }) + + it('should return empty array when permissionList is undefined but writePermission is defined', () => { + expect(getCollabEmailsWithPermission(undefined, true)).toEqual([]) + }) + + it('should return all collaborators when writePermission is undefined', () => { + const collabs: Permission[] = [ + { email: MOCK_EMAIL_1, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ] + const result = getCollabEmailsWithPermission(collabs) + expect(result).toEqual([MOCK_EMAIL_1, MOCK_EMAIL_2]) + }) + + it('should return write-only collaborators when writePermission is true', () => { + const collabs: Permission[] = [ + { email: MOCK_EMAIL_1, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ] + const result = getCollabEmailsWithPermission(collabs, true) + expect(result).toEqual([MOCK_EMAIL_1]) + }) + + it('should return read-only collaborators when writePermission is false', () => { + const collabs: Permission[] = [ + { email: MOCK_EMAIL_1, write: true }, + { email: MOCK_EMAIL_2, write: false }, + ] + const result = getCollabEmailsWithPermission(collabs, false) + expect(result).toEqual([MOCK_EMAIL_2]) + }) + }) + describe('removePrivateDetailsFromForm', () => { it('should correctly remove private details', async () => { // Act diff --git a/src/app/modules/form/admin-form/admin-form.utils.ts b/src/app/modules/form/admin-form/admin-form.utils.ts index a6eb347117..6aa2e1e2ff 100644 --- a/src/app/modules/form/admin-form/admin-form.utils.ts +++ b/src/app/modules/form/admin-form/admin-form.utils.ts @@ -3,7 +3,6 @@ import { err, ok, Result } from 'neverthrow' import { createLoggerWithLabel } from '../../../../config/logger' import { IPopulatedForm, ResponseMode, Status } from '../../../../types' -import { assertUnreachable } from '../../../utils/assert-unreachable' import { ApplicationError, DatabaseConflictError, @@ -205,8 +204,6 @@ export const getAssertPermissionFn = (level: PermissionLevel): AssertFormFn => { return assertHasWritePermissions case PermissionLevel.Delete: return assertHasDeletePermissions - default: - return assertUnreachable(level) } } diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts index f0b42ad4c1..52ed08bb87 100644 --- a/src/app/modules/form/form.service.ts +++ b/src/app/modules/form/form.service.ts @@ -4,7 +4,6 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../config/logger' import { IFormSchema, IPopulatedForm, Status } from '../../../types' import getFormModel from '../../models/form.server.model' -import { assertUnreachable } from '../../utils/assert-unreachable' import { getMongoErrorMessage } from '../../utils/handle-mongo-error' import { ApplicationError, DatabaseError } from '../core/core.errors' @@ -116,7 +115,5 @@ export const isFormPublic = ( return err(new FormDeletedError()) case Status.Private: return err(new PrivateFormError(form.inactiveMessage, form.title)) - default: - return assertUnreachable(form.status) } } diff --git a/src/app/modules/form/form.utils.ts b/src/app/modules/form/form.utils.ts index c983ecde6f..95a9f0b3df 100644 --- a/src/app/modules/form/form.utils.ts +++ b/src/app/modules/form/form.utils.ts @@ -1,7 +1,7 @@ import { pick } from 'lodash' import { Merge } from 'type-fest' -import { IPopulatedForm } from 'src/types' +import { IPopulatedForm, Permission } from '../../../types' // Kept in this file instead of form.types.ts so that this can be kept in sync // with FORM_PUBLIC_FIELDS more easily. @@ -55,3 +55,28 @@ export const removePrivateDetailsFromForm = ( admin: pick(form.admin, 'agency'), } } + +/** + * Extracts emails of collaborators, optionally filtering for a specific + * write permission. + * @param permissionList List of collaborators + * @param writePermission Optional write permission to filter on + * @returns Array of emails + */ +export const getCollabEmailsWithPermission = ( + permissionList?: Permission[], + writePermission?: boolean, +): string[] => { + if (!permissionList) { + return [] + } + return permissionList.reduce((acc, collaborator) => { + if ( + writePermission === undefined || + writePermission === collaborator.write + ) { + acc.push(collaborator.email) + } + return acc + }, []) +} diff --git a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts index b13aa6cdf4..357f777847 100644 --- a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts +++ b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts @@ -1,6 +1,7 @@ -import { ObjectID } from 'bson-ext' +import { ObjectId } from 'bson' import { times } from 'lodash' import mongoose from 'mongoose' +import { mocked } from 'ts-jest/utils' import getSubmissionModel from 'src/app/models/submission.server.model' import { @@ -9,16 +10,20 @@ import { } from 'src/app/modules/core/core.errors' import * as SubmissionService from 'src/app/modules/submission/submission.service' import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' +import MailService from 'src/app/services/mail/mail.service' import { createQueryWithDateParam } from 'src/app/utils/date' import * as LogicUtil from 'src/shared/util/logic' import { + AutoReplyOptions, BasicField, + IAttachmentInfo, IEmailFormSchema, IEmailSubmissionSchema, IEncryptedFormSchema, IEncryptedSubmissionSchema, IFormSchema, IPreventSubmitLogicSchema, + ISubmissionSchema, LogicType, ResponseMode, SubmissionType, @@ -26,6 +31,7 @@ import { import { generateDefaultField, + generateNewSingleAnswerResponse, generateProcessedSingleAnswerResponse, generateSingleAnswerResponse, } from 'tests/unit/backend/helpers/generate-form-data' @@ -34,14 +40,73 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db' import { ConflictError, ProcessingError, + SendEmailConfirmationError, ValidateFieldError, } from '../../submission.errors' +jest.mock('src/app/services/mail/mail.service') +const MockMailService = mocked(MailService, true) + const Submission = getSubmissionModel(mongoose) +const MOCK_FORM_ID = new ObjectId().toHexString() +const MOCK_SUBMISSION = { + _id: new ObjectId().toHexString(), + form: MOCK_FORM_ID, +} as ISubmissionSchema +const MOCK_AUTOREPLY_DATA = [ + { + question: 'Email', + answerTemplate: ['a@abc.com'], + }, +] +const AUTOREPLY_OPTIONS_1: AutoReplyOptions = { + hasAutoReply: true, + autoReplySubject: 'subject1', + autoReplySender: 'sender1', + autoReplyMessage: 'message1', + includeFormSummary: true, +} + +const AUTOREPLY_OPTIONS_2: AutoReplyOptions = { + hasAutoReply: true, + autoReplySubject: 'subject2', + autoReplySender: 'sender2', + autoReplyMessage: 'message2', + includeFormSummary: false, +} + +const MOCK_EMAIL_1 = 'a@abc.com' +const MOCK_EMAIL_2 = 'b@abc.com' + +const EXPECTED_AUTOREPLY_DATA_1 = { + email: MOCK_EMAIL_1, + subject: AUTOREPLY_OPTIONS_1.autoReplySubject, + sender: AUTOREPLY_OPTIONS_1.autoReplySender, + body: AUTOREPLY_OPTIONS_1.autoReplyMessage, + includeFormSummary: AUTOREPLY_OPTIONS_1.includeFormSummary, +} + +const EXPECTED_AUTOREPLY_DATA_2 = { + email: MOCK_EMAIL_2, + subject: AUTOREPLY_OPTIONS_2.autoReplySubject, + sender: AUTOREPLY_OPTIONS_2.autoReplySender, + body: AUTOREPLY_OPTIONS_2.autoReplyMessage, + includeFormSummary: AUTOREPLY_OPTIONS_2.includeFormSummary, +} + +const MOCK_ATTACHMENTS: IAttachmentInfo[] = [ + { + filename: 'filename', + content: Buffer.from('attachment1'), + fieldId: new ObjectId().toHexString(), + }, +] + describe('submission.service', () => { beforeAll(async () => await dbHandler.connect()) afterAll(async () => await dbHandler.closeDatabase()) + beforeEach(() => jest.clearAllMocks()) describe('getProcessedResponses', () => { it('should return list of parsed responses for encrypted form submission successfully', async () => { @@ -324,7 +389,7 @@ describe('submission.service', () => { describe('getFormSubmissionsCount', () => { const countSpy = jest.spyOn(Submission, 'countDocuments') - const MOCK_FORM_ID = new ObjectID() + const MOCK_FORM_ID = new ObjectId() beforeEach(async () => { await dbHandler.clearCollection(Submission.collection.name) @@ -482,4 +547,436 @@ describe('submission.service', () => { expect(actualResult._unsafeUnwrapErr()).toBeInstanceOf(DatabaseError) }) }) + + describe('sendEmailConfirmations', () => { + it('should call mail service and return true when email confirmations are sent successfully', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_2, + }, + ], + } as unknown) as IFormSchema + MockMailService.sendAutoReplyEmails.mockResolvedValueOnce([ + { + status: 'fulfilled', + value: true, + }, + { + status: 'fulfilled', + value: true, + }, + ]) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + const expectedAutoReplyData = [ + EXPECTED_AUTOREPLY_DATA_1, + EXPECTED_AUTOREPLY_DATA_2, + ] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + responsesData: MOCK_AUTOREPLY_DATA, + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should not call mail service when there are no email fields', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [generateDefaultField(BasicField.Number)], + } as unknown) as IFormSchema + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Number, { + _id: mockForm.form_fields![0]._id, + }), + }, + ] + + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + expect(MockMailService.sendAutoReplyEmails).not.toHaveBeenCalled() + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should not call mail service when there are email fields but all without email confirmation', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: { hasAutoReply: false }, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: { hasAutoReply: false }, + }, + ], + } as unknown) as IFormSchema + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + expect(MockMailService.sendAutoReplyEmails).not.toHaveBeenCalled() + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should call mail service when there is a mix of email fields with and without confirmation', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: { hasAutoReply: false }, + }, + ], + } as unknown) as IFormSchema + MockMailService.sendAutoReplyEmails.mockResolvedValueOnce([ + { + status: 'fulfilled', + value: true, + }, + ]) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + const expectedAutoReplyData = [EXPECTED_AUTOREPLY_DATA_1] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + responsesData: MOCK_AUTOREPLY_DATA, + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should call mail service with responsesData empty when autoReplyData is undefined', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_2, + }, + ], + } as unknown) as IFormSchema + MockMailService.sendAutoReplyEmails.mockResolvedValueOnce([ + { + status: 'fulfilled', + value: true, + }, + { + status: 'fulfilled', + value: true, + }, + ]) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: undefined, + }) + + const expectedAutoReplyData = [ + EXPECTED_AUTOREPLY_DATA_1, + EXPECTED_AUTOREPLY_DATA_2, + ] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + responsesData: [], + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should call mail service with attachments undefined when there are no attachments', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_2, + }, + ], + } as unknown) as IFormSchema + MockMailService.sendAutoReplyEmails.mockResolvedValueOnce([ + { + status: 'fulfilled', + value: true, + }, + { + status: 'fulfilled', + value: true, + }, + ]) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: undefined, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + const expectedAutoReplyData = [ + EXPECTED_AUTOREPLY_DATA_1, + EXPECTED_AUTOREPLY_DATA_2, + ] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: undefined, + responsesData: MOCK_AUTOREPLY_DATA, + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrap()).toBe(true) + }) + + it('should return SendEmailConfirmationError when mail service errors', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_2, + }, + ], + } as unknown) as IFormSchema + MockMailService.sendAutoReplyEmails.mockImplementationOnce(() => + Promise.reject('rejected'), + ) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + const expectedAutoReplyData = [ + EXPECTED_AUTOREPLY_DATA_1, + EXPECTED_AUTOREPLY_DATA_2, + ] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + responsesData: MOCK_AUTOREPLY_DATA, + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrapErr()).toEqual( + new SendEmailConfirmationError(), + ) + }) + + it('should return SendEmailConfirmationError when any email confirmations fail', async () => { + const mockForm = ({ + _id: MOCK_FORM_ID, + form_fields: [ + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_1, + }, + { + ...generateDefaultField(BasicField.Email), + autoReplyOptions: AUTOREPLY_OPTIONS_2, + }, + ], + } as unknown) as IFormSchema + const mockReason = 'reason' + MockMailService.sendAutoReplyEmails.mockResolvedValueOnce([ + { + status: 'fulfilled', + value: true, + }, + { + status: 'rejected', + reason: mockReason, + }, + ]) + + const responses = [ + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![0]._id, + answer: MOCK_EMAIL_1, + }), + }, + { + ...generateNewSingleAnswerResponse(BasicField.Email, { + _id: mockForm.form_fields![1]._id, + answer: MOCK_EMAIL_2, + }), + }, + ] + const result = await SubmissionService.sendEmailConfirmations({ + form: mockForm, + parsedResponses: responses, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + autoReplyData: MOCK_AUTOREPLY_DATA, + }) + + const expectedAutoReplyData = [ + EXPECTED_AUTOREPLY_DATA_1, + EXPECTED_AUTOREPLY_DATA_2, + ] + + expect(MockMailService.sendAutoReplyEmails).toHaveBeenCalledWith({ + form: mockForm, + submission: MOCK_SUBMISSION, + attachments: MOCK_ATTACHMENTS, + responsesData: MOCK_AUTOREPLY_DATA, + autoReplyMailDatas: expectedAutoReplyData, + }) + expect(result._unsafeUnwrapErr()).toEqual( + new SendEmailConfirmationError(), + ) + }) + }) }) diff --git a/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts b/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts index fe448f4340..63012c7dec 100644 --- a/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts +++ b/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts @@ -44,14 +44,5 @@ describe('submission.utils', () => { ) expect(actual.length).toEqual(2) }) - - it('should throw error if called with invalid responseMode', async () => { - // Arrange - const invalidResponseMode = 'something' as ResponseMode - // Act + Assert - expect(() => getModeFilter(invalidResponseMode)).toThrowError( - `This should never be reached in TypeScript: "${invalidResponseMode}"`, - ) - }) }) }) diff --git a/src/app/modules/submission/email-submission/__tests__/email-submission.routes.spec.ts b/src/app/modules/submission/email-submission/__tests__/email-submission.routes.spec.ts index bfa249996f..b9268b5acc 100644 --- a/src/app/modules/submission/email-submission/__tests__/email-submission.routes.spec.ts +++ b/src/app/modules/submission/email-submission/__tests__/email-submission.routes.spec.ts @@ -1,22 +1,37 @@ import SPCPAuthClient from '@opengovsg/spcp-auth-client' import { celebrate, Joi } from 'celebrate' import { RequestHandler, Router } from 'express' +import { omit } from 'lodash' import session, { Session } from 'supertest-session' import { mocked } from 'ts-jest/utils' import * as FormController from 'src/app/controllers/forms.server.controller' import * as MyInfoController from 'src/app/controllers/myinfo.server.controller' -import * as SubmissionsController from 'src/app/controllers/submissions.server.controller' import * as PublicFormMiddleware from 'src/app/modules/form/public-form/public-form.middlewares' import * as SpcpController from 'src/app/modules/spcp/spcp.controller' import * as EmailSubmissionsMiddleware from 'src/app/modules/submission/email-submission/email-submission.middleware' +import * as SubmissionsMiddleware from 'src/app/modules/submission/submission.middleware' import { CaptchaFactory } from 'src/app/services/captcha/captcha.factory' import * as CaptchaMiddleware from 'src/app/services/captcha/captcha.middleware' -import { AuthType, BasicField, Status } from 'src/types' +import { AuthType, BasicField, IFieldSchema, Status } from 'src/types' import { setupApp } from 'tests/integration/helpers/express-setup' import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { + MOCK_ATTACHMENT_FIELD, + MOCK_ATTACHMENT_RESPONSE, + MOCK_CHECKBOX_FIELD, + MOCK_CHECKBOX_RESPONSE, + MOCK_NO_RESPONSES_BODY, + MOCK_OPTIONAL_VERIFIED_FIELD, + MOCK_OPTIONAL_VERIFIED_RESPONSE, + MOCK_SECTION_FIELD, + MOCK_SECTION_RESPONSE, + MOCK_TEXT_FIELD, + MOCK_TEXTFIELD_RESPONSE, +} from './email-submission.test.constants' + jest.mock('@opengovsg/spcp-auth-client') const MockAuthClient = mocked(SPCPAuthClient, true) const mockSpClient = mocked(MockAuthClient.mock.instances[0], true) @@ -32,11 +47,6 @@ jest.mock('nodemailer', () => ({ const SUBMISSIONS_ENDPT_BASE = '/v2/submissions/email' const SUBMISSIONS_ENDPT = `${SUBMISSIONS_ENDPT_BASE}/:formId([a-fA-F0-9]{24})` -const MOCK_SUBMISSION_BODY = { - responses: [], - isPreview: false, -} - const EmailSubmissionsRouter = Router() EmailSubmissionsRouter.post( SUBMISSIONS_ENDPT, @@ -53,7 +63,9 @@ EmailSubmissionsRouter.post( Joi.object() .keys({ _id: Joi.string().required(), - question: Joi.string().required(), + // Question is optional in anticipation of the same change + // when merging middlewares into controller + question: Joi.string(), fieldType: Joi.string() .required() .valid(...Object.values(BasicField)), @@ -74,12 +86,11 @@ EmailSubmissionsRouter.post( }), EmailSubmissionsMiddleware.validateEmailSubmission, MyInfoController.verifyMyInfoVals as RequestHandler, - (SubmissionsController.injectAutoReplyInfo as unknown) as RequestHandler, SpcpController.appendVerifiedSPCPResponses as RequestHandler, EmailSubmissionsMiddleware.prepareEmailSubmission as RequestHandler, EmailSubmissionsMiddleware.saveMetadataToDb, EmailSubmissionsMiddleware.sendAdminEmail, - SubmissionsController.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations as RequestHandler, ) const EmailSubmissionsApp = setupApp('/', EmailSubmissionsRouter) @@ -97,6 +108,395 @@ describe('email-submission.routes', () => { }) afterAll(async () => await dbHandler.closeDatabase()) + describe('Joi validation', () => { + it('should return 200 when submission is valid', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [MOCK_TEXT_FIELD], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + // MOCK_RESPONSE contains all required keys + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [MOCK_TEXTFIELD_RESPONSE], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 200 when answer is empty string for optional field', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [ + { ...MOCK_TEXT_FIELD, required: false } as IFieldSchema, + ], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [{ ...MOCK_TEXTFIELD_RESPONSE, answer: '' }], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 200 when attachment response has filename and content', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [MOCK_ATTACHMENT_FIELD], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [ + { + ...MOCK_ATTACHMENT_RESPONSE, + content: MOCK_ATTACHMENT_RESPONSE.content.toString('binary'), + }, + ], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 200 when response has isHeader key', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [MOCK_SECTION_FIELD], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [{ ...MOCK_SECTION_RESPONSE, isHeader: true }], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 200 when signature is empty string for optional verified field', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [MOCK_OPTIONAL_VERIFIED_FIELD], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [{ ...MOCK_OPTIONAL_VERIFIED_RESPONSE, signature: '' }], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 200 when response has answerArray and no answer', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + form_fields: [MOCK_CHECKBOX_FIELD], + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [MOCK_CHECKBOX_RESPONSE], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + + it('should return 400 when isPreview key is missing', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + // Note missing isPreview + .field('body', JSON.stringify({ responses: [] })) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when responses key is missing', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + // Note missing responses + .field('body', JSON.stringify({ isPreview: false })) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when response is missing _id', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [omit(MOCK_TEXTFIELD_RESPONSE, '_id')], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when response is missing fieldType', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [omit(MOCK_TEXTFIELD_RESPONSE, 'fieldType')], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when response has invalid fieldType', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [ + { ...MOCK_TEXTFIELD_RESPONSE, fieldType: 'definitelyInvalid' }, + ], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when response is missing answer', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [omit(MOCK_TEXTFIELD_RESPONSE, 'answer')], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when response has both answer and answerArray', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [{ ...MOCK_TEXTFIELD_RESPONSE, answerArray: [] }], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when attachment response has filename but not content', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [omit(MOCK_ATTACHMENT_RESPONSE), 'content'], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + + it('should return 400 when attachment response has content but not filename', async () => { + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: Status.Public, + }, + }) + + const response = await request + .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) + .field( + 'body', + JSON.stringify({ + isPreview: false, + responses: [omit(MOCK_ATTACHMENT_RESPONSE), 'filename'], + }), + ) + .query({ captchaResponse: 'null' }) + + expect(response.status).toBe(400) + expect(response.body.message).toEqual( + 'celebrate request validation failed', + ) + }) + }) + describe('SPCP authentication', () => { describe('SingPass', () => { it('should return 200 when submission is valid', async () => { @@ -116,7 +516,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtSp=mockJwt']) @@ -139,7 +539,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) // Note cookie is not set @@ -163,7 +563,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) // Note cookie is for CorpPass, not SingPass .set('Cookie', ['jwtCp=mockJwt']) @@ -192,7 +592,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtSp=mockJwt']) @@ -222,7 +622,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtSp=mockJwt']) @@ -254,7 +654,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtCp=mockJwt']) @@ -277,7 +677,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) // Note cookie is not set @@ -301,7 +701,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) // Note cookie is for SingPass, not CorpPass .set('Cookie', ['jwtSp=mockJwt']) @@ -330,7 +730,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtCp=mockJwt']) @@ -360,7 +760,7 @@ describe('email-submission.routes', () => { const response = await request .post(`${SUBMISSIONS_ENDPT_BASE}/${form._id}`) - .field('body', JSON.stringify(MOCK_SUBMISSION_BODY)) + .field('body', JSON.stringify(MOCK_NO_RESPONSES_BODY)) .query({ captchaResponse: 'null' }) .set('Cookie', ['jwtCp=mockJwt']) diff --git a/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts b/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts index a9f9469adb..2579bf0d63 100644 --- a/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts +++ b/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts @@ -705,4 +705,92 @@ describe('email-submission.service', () => { expect(result._unsafeUnwrapErr()).toEqual(new SendAdminEmailError()) }) }) + + describe('extractEmailAnswers', () => { + const MOCK_ANSWER_1 = 'mockAnswer1' + const MOCK_ANSWER_2 = 'mockAnswer2' + it('should include an email field with an answer when there is a single email field', () => { + const result = EmailSubmissionService.extractEmailAnswers([ + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: MOCK_ANSWER_1, + }, + ]) + expect(result).toEqual([MOCK_ANSWER_1]) + }) + + it('should include all email fields when all have answers', () => { + const result = EmailSubmissionService.extractEmailAnswers([ + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: MOCK_ANSWER_1, + }, + { + _id: new ObjectId().toHexString(), + question: 'email2', + fieldType: BasicField.Email, + answer: MOCK_ANSWER_2, + }, + ]) + expect(result).toEqual([MOCK_ANSWER_1, MOCK_ANSWER_2]) + }) + + it('should include only the email fields with answers when some do not have answers', () => { + const result = EmailSubmissionService.extractEmailAnswers([ + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: MOCK_ANSWER_1, + }, + { + _id: new ObjectId().toHexString(), + question: 'email2', + fieldType: BasicField.Email, + answer: '', + }, + ]) + expect(result).toEqual([MOCK_ANSWER_1]) + }) + + it('should include no fields when no email fields have answers', () => { + const result = EmailSubmissionService.extractEmailAnswers([ + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: '', + }, + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: '', + }, + ]) + expect(result).toEqual([]) + }) + + it('should not include non-email fields', () => { + const result = EmailSubmissionService.extractEmailAnswers([ + { + _id: new ObjectId().toHexString(), + question: 'number', + fieldType: BasicField.Number, + answer: MOCK_ANSWER_1, + }, + { + _id: new ObjectId().toHexString(), + question: 'email', + fieldType: BasicField.Email, + answer: MOCK_ANSWER_1, + }, + ]) + expect(result).toEqual([MOCK_ANSWER_1]) + }) + }) }) diff --git a/src/app/modules/submission/email-submission/__tests__/email-submission.test.constants.ts b/src/app/modules/submission/email-submission/__tests__/email-submission.test.constants.ts new file mode 100644 index 0000000000..c558d82956 --- /dev/null +++ b/src/app/modules/submission/email-submission/__tests__/email-submission.test.constants.ts @@ -0,0 +1,54 @@ +import fs from 'fs' + +import { + BasicField, + IAttachmentFieldSchema, + ICheckboxFieldSchema, + IField, +} from 'src/types' + +import { + generateAttachmentResponse, + generateCheckboxResponse, + generateDefaultField, + generateSingleAnswerResponse, +} from 'tests/unit/backend/helpers/generate-form-data' + +export const MOCK_NO_RESPONSES_BODY = { + responses: [], + isPreview: false, +} + +export const MOCK_TEXT_FIELD = generateDefaultField(BasicField.ShortText) +export const MOCK_TEXTFIELD_RESPONSE = generateSingleAnswerResponse( + MOCK_TEXT_FIELD, +) + +export const MOCK_ATTACHMENT_FIELD = generateDefaultField(BasicField.Attachment) +export const MOCK_ATTACHMENT_RESPONSE = generateAttachmentResponse( + MOCK_ATTACHMENT_FIELD as IAttachmentFieldSchema, + 'valid.txt', + fs.readFileSync('tests/unit/backend/resources/valid.txt'), +) + +export const MOCK_SECTION_FIELD = generateDefaultField(BasicField.Section) +export const MOCK_SECTION_RESPONSE = generateSingleAnswerResponse( + MOCK_SECTION_FIELD, +) + +export const MOCK_CHECKBOX_FIELD = generateDefaultField(BasicField.Checkbox) +export const MOCK_CHECKBOX_RESPONSE = generateCheckboxResponse( + MOCK_CHECKBOX_FIELD as ICheckboxFieldSchema, +) + +export const MOCK_OPTIONAL_VERIFIED_FIELD = generateDefaultField( + BasicField.Email, + { + isVerifiable: true, + required: false, + } as Partial, +) +export const MOCK_OPTIONAL_VERIFIED_RESPONSE = generateSingleAnswerResponse( + MOCK_OPTIONAL_VERIFIED_FIELD, + '', +) diff --git a/src/app/modules/submission/email-submission/email-submission.errors.ts b/src/app/modules/submission/email-submission/email-submission.errors.ts index 7681bfb823..372953651c 100644 --- a/src/app/modules/submission/email-submission/email-submission.errors.ts +++ b/src/app/modules/submission/email-submission/email-submission.errors.ts @@ -1,17 +1,26 @@ import { ApplicationError } from '../../core/core.errors' +/** + * Error in headers passed to Busboy constructor + */ export class InitialiseMultipartReceiverError extends ApplicationError { constructor(message = 'Error while initialising multipart receiver') { super(message) } } +/** + * Submission size too large + */ export class MultipartContentLimitError extends ApplicationError { constructor(message = 'Multipart content size limit exceeded') { super(message) } } +/** + * Could not parse response body + */ export class MultipartContentParsingError extends ApplicationError { constructor(message = 'Could not parse multipart form content') { super(message) @@ -24,12 +33,18 @@ export class MultipartError extends ApplicationError { } } +/** + * Generic error for errors thrown while receiving multipart form data + */ export class InvalidFileExtensionError extends ApplicationError { constructor(message = 'Invalid file extension found in attachment') { super(message) } } +/** + * Attachment greater than size limit + */ export class AttachmentTooLargeError extends ApplicationError { constructor(message = 'Attachment size limit exceeded') { super(message) @@ -42,6 +57,9 @@ export class SubmissionHashError extends ApplicationError { } } +/** + * Error while hashing responses in order to save submission hash to database + */ export class ConcatSubmissionError extends ApplicationError { constructor( message = 'Error occurred while concatenating responses to attachments', diff --git a/src/app/modules/submission/email-submission/email-submission.middleware.ts b/src/app/modules/submission/email-submission/email-submission.middleware.ts index 52829379b6..49ddabc1bb 100644 --- a/src/app/modules/submission/email-submission/email-submission.middleware.ts +++ b/src/app/modules/submission/email-submission/email-submission.middleware.ts @@ -4,7 +4,16 @@ import { StatusCodes } from 'http-status-codes' import { Merge, SetOptional } from 'type-fest' import { createLoggerWithLabel } from '../../../../config/logger' -import { FieldResponse, ResWithHashedFields, WithForm } from '../../../../types' +import { + EmailData, + FieldResponse, + ResWithHashedFields, + WithAttachments, + WithEmailData, + WithEmailModeMetadata, + WithForm, + WithSubmission, +} from '../../../../types' import { createReqMeta } from '../../../utils/request' import { getProcessedResponses } from '../submission.service' import { @@ -15,14 +24,7 @@ import { import * as EmailSubmissionReceiver from './email-submission.receiver' import * as EmailSubmissionService from './email-submission.service' -import { - EmailData, - WithAdminEmailData, - WithAttachments, - WithEmailData, - WithFormMetadata, - WithSubmission, -} from './email-submission.types' +import { WithAdminEmailData } from './email-submission.types' import { mapAttachmentsFromResponses, mapRouteError, @@ -191,7 +193,9 @@ export const saveMetadataToDb: RequestHandler< ParamsDictionary, { message: string; spcpSubmissionFailure: boolean } > = async (req, res, next) => { - const { form, attachments, formData } = req as WithFormMetadata + const { form, attachments, formData } = req as WithEmailModeMetadata< + typeof req + > const logMeta = { action: 'saveMetadataToDb', formId: form._id, @@ -228,12 +232,25 @@ export const saveMetadataToDb: RequestHandler< }) } +/** + * Sends response email to admin + * @param req - Express request object + * @param req.form - form object from req + * @param req.formData - the submission for the form + * @param req.jsonData - data to be included in JSON section of email + * @param req.submission - submission which was saved to database + * @param req.attachments - submitted attachments, parsed by + * exports.receiveSubmission + * @param res - Express response object + * @param next - the next expressjs callback, invoked once attachments + * are processed + */ export const sendAdminEmail: RequestHandler< ParamsDictionary, - { message: string; spcpSubmissionFailure: boolean } + { message: string; spcpSubmissionFailure: boolean }, + { parsedResponses: ProcessedFieldResponse[] } > = async (req, res, next) => { const { - replyToEmails, form, formData, jsonData, @@ -252,7 +269,9 @@ export const sendAdminEmail: RequestHandler< meta: logMeta, }) return EmailSubmissionService.sendSubmissionToAdmin({ - replyToEmails, + replyToEmails: EmailSubmissionService.extractEmailAnswers( + req.body.parsedResponses, + ), form, submission, attachments, diff --git a/src/app/modules/submission/email-submission/email-submission.receiver.ts b/src/app/modules/submission/email-submission/email-submission.receiver.ts index 7e70df418b..bc741651b3 100644 --- a/src/app/modules/submission/email-submission/email-submission.receiver.ts +++ b/src/app/modules/submission/email-submission/email-submission.receiver.ts @@ -3,6 +3,7 @@ import { IncomingHttpHeaders } from 'http' import { err, ok, Result, ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../../config/logger' +import { IAttachmentInfo } from '../../../../types' import { MB } from '../../../constants/filesize' import { @@ -11,7 +12,7 @@ import { MultipartContentParsingError, MultipartError, } from './email-submission.errors' -import { IAttachmentInfo, ParsedMultipartForm } from './email-submission.types' +import { ParsedMultipartForm } from './email-submission.types' import { addAttachmentToResponses, handleDuplicatesInAttachments, diff --git a/src/app/modules/submission/email-submission/email-submission.service.ts b/src/app/modules/submission/email-submission/email-submission.service.ts index 2af418fd42..7612f3ca16 100644 --- a/src/app/modules/submission/email-submission/email-submission.service.ts +++ b/src/app/modules/submission/email-submission/email-submission.service.ts @@ -1,11 +1,15 @@ import crypto from 'crypto' -import { sumBy } from 'lodash' import mongoose from 'mongoose' import { errAsync, okAsync, ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../../config/logger' import { + BasicField, + EmailData, + EmailDataForOneField, + EmailFormField, FieldResponse, + IAttachmentInfo, IEmailFormSchema, IEmailSubmissionSchema, SubmissionType, @@ -33,16 +37,9 @@ import { InvalidFileExtensionError, SubmissionHashError, } from './email-submission.errors' +import { SubmissionHash } from './email-submission.types' import { - EmailAutoReplyField, - EmailData, - EmailDataForOneField, - EmailFormField, - EmailJsonField, - IAttachmentInfo, - SubmissionHash, -} from './email-submission.types' -import { + areAttachmentsMoreThan7MB, concatAttachmentsAndResponses, getAnswerForCheckbox, getAnswerRowsForTable, @@ -59,6 +56,7 @@ const logger = createLoggerWithLabel(module) * Helper function for createEmailData. * @param response Processed and validated response for one field * @param hashedFields IDs of fields whose responses have been verified by MyInfo hashes + * @returns email data for one form field */ const createEmailDataForOneField = ( response: ProcessedFieldResponse, @@ -80,6 +78,7 @@ const createEmailDataForOneField = ( * Creates data to be included in the response and autoreply emails. * @param parsedResponses Processed and validated responses * @param hashedFields IDs of fields whose responses have been verified by MyInfo hashes + * @returns email data for admin response and email confirmations */ export const createEmailData = ( parsedResponses: ProcessedFieldResponse[], @@ -93,7 +92,7 @@ export const createEmailData = ( parsedResponses .flatMap((response) => createEmailDataForOneField(response, hashedFields)) // Then reshape such that autoReplyData, jsonData and formData are each arrays - .reduce( + .reduce( (acc, dataForOneField) => { if (dataForOneField.autoReplyData) { acc.autoReplyData.push(dataForOneField.autoReplyData) @@ -105,9 +104,9 @@ export const createEmailData = ( return acc }, { - autoReplyData: [] as EmailAutoReplyField[], - jsonData: [] as EmailJsonField[], - formData: [] as EmailFormField[], + autoReplyData: [], + jsonData: [], + formData: [], }, ) ) @@ -117,15 +116,16 @@ export const createEmailData = ( * Validates that the attachments in a submission do not violate form-level * constraints e.g. form-wide attachment size limit. * @param parsedResponses Unprocessed responses + * @returns okAsync(true) if validation passes + * @returns errAsync(InvalidFileExtensionError) if invalid file extensions are found + * @returns errAsync(AttachmentTooLargeError) if total attachment size exceeds 7MB */ export const validateAttachments = ( parsedResponses: FieldResponse[], ): ResultAsync => { const logMeta = { action: 'validateAttachments' } const attachments = mapAttachmentsFromResponses(parsedResponses) - // Check if total attachments size is < 7mb - const totalAttachmentSize = sumBy(attachments, (a) => a.content.byteLength) - if (totalAttachmentSize > 7000000) { + if (areAttachmentsMoreThan7MB(attachments)) { logger.error({ message: 'Attachment size is too large', meta: logMeta, @@ -161,6 +161,9 @@ export const validateAttachments = ( * Creates hash of a submission * @param formData Responses sent to admin * @param attachments Attachments in response + * @returns okAsync(hash and salt) if hashing was successful + * @returns errAsync(SubmissionHashError) if error occurred while hashing + * @returns errAsync(ConcatSubmissionError) if error occurred while concatenating attachments */ export const hashSubmission = ( formData: EmailFormField[], @@ -216,6 +219,8 @@ export const hashSubmission = ( * Saves an email submission to the database. * @param form * @param submissionHash Hash of submission and salt + * @returns okAsync(the saved document) if submission was saved successfully + * @returns errAsync(DatabaseError) if submission failed to be saved */ export const saveSubmissionMetadata = ( form: IEmailFormSchema, @@ -246,6 +251,12 @@ export const saveSubmissionMetadata = ( ) } +/** + * Sends email mode response to admin + * @param adminEmailParams Parameters to be passed on to mail service + * @returns okAsync(true) if response was sent successfully to all recipients + * @returns errAsync(SendAdminEmailError) if at least one email failed to be sent + */ export const sendSubmissionToAdmin = ( adminEmailParams: Parameters[0], ): ResultAsync => { @@ -265,3 +276,19 @@ export const sendSubmissionToAdmin = ( }, ) } + +/** + * Extracts an array of answers to email fields + * @param parsedResponses All form responses + * @returns an array of all email field responses + */ +export const extractEmailAnswers = ( + parsedResponses: ProcessedFieldResponse[], +): string[] => { + return parsedResponses.reduce((acc, response) => { + if (response.fieldType === BasicField.Email && response.answer) { + acc.push(response.answer) + } + return acc + }, []) +} diff --git a/src/app/modules/submission/email-submission/email-submission.types.ts b/src/app/modules/submission/email-submission/email-submission.types.ts index bbc34f768a..2d66820eae 100644 --- a/src/app/modules/submission/email-submission/email-submission.types.ts +++ b/src/app/modules/submission/email-submission/email-submission.types.ts @@ -2,40 +2,11 @@ import { BasicField, FieldResponse, IBaseResponse, - IEmailFormSchema, - IEmailSubmissionSchema, + WithEmailModeMetadata, + WithSubmission, } from '../../../../types' import { ProcessedResponse } from '../submission.types' -export interface EmailAutoReplyField { - question: string - answerTemplate: string[] -} - -export interface EmailJsonField { - question: string - answer: string -} - -export interface EmailFormField { - question: string - answer: string - fieldType: BasicField - answerTemplate: string[] -} - -export interface EmailData { - autoReplyData: EmailAutoReplyField[] - jsonData: EmailJsonField[] - formData: EmailFormField[] -} - -export interface EmailDataForOneField { - autoReplyData?: EmailAutoReplyField - jsonData?: EmailJsonField - formData: EmailFormField -} - // When a response has been formatted for email, all answerArray // should have been converted to answer interface IResponseFormattedForEmail extends IBaseResponse { @@ -47,35 +18,13 @@ interface IResponseFormattedForEmail extends IBaseResponse { export type ResponseFormattedForEmail = IResponseFormattedForEmail & ProcessedResponse -export type WithEmailData = T & EmailData - export interface ParsedMultipartForm { responses: FieldResponse[] } -export interface IAttachmentInfo { - filename: string - content: Buffer - fieldId: string -} - -export type WithAttachments = T & { attachments: IAttachmentInfo[] } - export interface SubmissionHash { hash: string salt: string } -export type WithEmailForm = T & { form: IEmailFormSchema } - -export type WithFormMetadata = WithEmailData & - WithAttachments & - WithEmailForm - -export type WithSubmission = T & { submission: IEmailSubmissionSchema } - -export type WithAutoReplyEmails = T & { replyToEmails: string[] | undefined } - -export type WithAdminEmailData = WithFormMetadata & - WithSubmission & - WithAutoReplyEmails +export type WithAdminEmailData = WithEmailModeMetadata & WithSubmission diff --git a/src/app/modules/submission/email-submission/email-submission.util.ts b/src/app/modules/submission/email-submission/email-submission.util.ts index 121347166b..bd883e6ef2 100644 --- a/src/app/modules/submission/email-submission/email-submission.util.ts +++ b/src/app/modules/submission/email-submission/email-submission.util.ts @@ -5,7 +5,12 @@ import { FilePlatforms } from '../../../../shared/constants' import * as FileValidation from '../../../../shared/util/file-validation' import { BasicField, + EmailAutoReplyField, + EmailDataForOneField, + EmailFormField, + EmailJsonField, FieldResponse, + IAttachmentInfo, IAttachmentResponse, MapRouteError, } from '../../../../types' @@ -35,20 +40,14 @@ import { MultipartError, SubmissionHashError, } from './email-submission.errors' -import { - EmailAutoReplyField, - EmailDataForOneField, - EmailFormField, - EmailJsonField, - IAttachmentInfo, - ResponseFormattedForEmail, -} from './email-submission.types' +import { ResponseFormattedForEmail } from './email-submission.types' /** * Determines the prefix for a question based on whether it is verified * by MyInfo. * @param response * @param hashedFields Hash for verifying MyInfo fields + * @returns the prefix */ const getMyInfoPrefix = ( response: ResponseFormattedForEmail, @@ -89,6 +88,7 @@ const getFieldTypePrefix = (response: ResponseFormattedForEmail): string => { * Transforms a question for inclusion in the JSON data used by the * data collation tool. * @param response + * @returns the prefixed question for this response */ export const getJsonPrefixedQuestion = ( response: ResponseFormattedForEmail, @@ -101,6 +101,7 @@ export const getJsonPrefixedQuestion = ( * Transforms a question for inclusion in the admin email table. * @param response * @param hashedFields + * @returns the joined prefixes for the question in the given response */ export const getFormDataPrefixedQuestion = ( response: ResponseFormattedForEmail, @@ -210,7 +211,7 @@ export const getFormattedResponse = ( * zip files are checked recursively. * * @param attachments - Array of file objects - * @return Whether all attachments are valid + * @returns Whether all attachments are valid */ export const getInvalidFileExtensions = ( attachments: IAttachmentInfo[], @@ -232,6 +233,11 @@ export const getInvalidFileExtensions = ( return Promise.all(promises).then((results) => flattenDeep(results)) } +/** + * Checks whether the total size of attachments exceeds 7MB + * @param attachments List of attachments + * @returns true if total attachment size exceeds 7MB + */ export const areAttachmentsMoreThan7MB = ( attachments: IAttachmentInfo[], ): boolean => { @@ -249,6 +255,10 @@ const isAttachmentResponse = ( ) } +/** + * Extracts attachment fields from form responses + * @param responses Form responses + */ export const mapAttachmentsFromResponses = ( responses: FieldResponse[], ): IAttachmentInfo[] => { @@ -318,6 +328,12 @@ export const mapRouteError: MapRouteError = (error) => { } } +/** + * Checks whether attachmentMap contains the given response + * @param attachmentMap Map of field IDs to attachments + * @param response The response to check + * @returns true if response is in map, false otherwise + */ const isAttachmentResponseFromMap = ( attachmentMap: Record, response: FieldResponse, @@ -332,6 +348,7 @@ const isAttachmentResponseFromMap = ( * * @param responses - Array of responses received * @param attachments - Array of file objects + * @returns void. Modifies responses in place. */ export const addAttachmentToResponses = ( responses: FieldResponse[], @@ -365,6 +382,7 @@ export const addAttachmentToResponses = ( * One of the duplicated files will not have its name changed. * Two abc.txt will become 1-abc.txt and abc.txt * @param attachments - Array of file objects + * @returns void. Modifies array in-place. */ export const handleDuplicatesInAttachments = ( attachments: IAttachmentInfo[], @@ -394,7 +412,7 @@ export const handleDuplicatesInAttachments = ( * Concatenate response into a string for hashing * @param formData Field-value tuples for admin email * @param attachments Array of attachments as buffers - * @return concatenated response to hash + * @returns concatenated response to hash */ export const concatAttachmentsAndResponses = ( formData: EmailFormField[], diff --git a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.routes.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.routes.spec.ts index 8bfa54df4e..6b3fad5176 100644 --- a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.routes.spec.ts +++ b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.routes.spec.ts @@ -7,11 +7,11 @@ import { mocked } from 'ts-jest/utils' import * as encryptSubmissions from 'src/app/controllers/encrypt-submissions.server.controller' import * as FormController from 'src/app/controllers/forms.server.controller' import * as MyInfoController from 'src/app/controllers/myinfo.server.controller' -import * as SubmissionsController from 'src/app/controllers/submissions.server.controller' import * as webhookVerifiedContentFactory from 'src/app/factories/webhook-verified-content.factory' import * as PublicFormMiddleware from 'src/app/modules/form/public-form/public-form.middlewares' import * as SpcpController from 'src/app/modules/spcp/spcp.controller' import * as EncryptSubmissionsMiddleware from 'src/app/modules/submission/encrypt-submission/encrypt-submission.middleware' +import * as SubmissionsMiddleware from 'src/app/modules/submission/submission.middleware' import { CaptchaFactory } from 'src/app/services/captcha/captcha.factory' import * as CaptchaMiddleware from 'src/app/services/captcha/captcha.middleware' import { AuthType, BasicField, Status } from 'src/types' @@ -93,12 +93,11 @@ EncryptSubmissionsRouter.post( EncryptSubmissionsMiddleware.validateAndProcessEncryptSubmission, SpcpController.isSpcpAuthenticated, MyInfoController.verifyMyInfoVals as RequestHandler, - (SubmissionsController.injectAutoReplyInfo as unknown) as RequestHandler, webhookVerifiedContentFactory.encryptedVerifiedFields as RequestHandler, EncryptSubmissionsMiddleware.prepareEncryptSubmission as RequestHandler, (encryptSubmissions.saveResponseToDb as unknown) as RequestHandler, webhookVerifiedContentFactory.post as RequestHandler, - SubmissionsController.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations as RequestHandler, ) const EncryptSubmissionsApp = setupApp('/', EncryptSubmissionsRouter) diff --git a/src/app/modules/submission/submission.errors.ts b/src/app/modules/submission/submission.errors.ts index 17f7920df9..edb7757a14 100644 --- a/src/app/modules/submission/submission.errors.ts +++ b/src/app/modules/submission/submission.errors.ts @@ -50,3 +50,12 @@ export class SendAdminEmailError extends ApplicationError { super(message) } } + +/** + * Error while sending confirmation email to recipients. + */ +export class SendEmailConfirmationError extends ApplicationError { + constructor(message = 'Error while sending confirmation emails') { + super(message) + } +} diff --git a/src/app/modules/submission/submission.middleware.ts b/src/app/modules/submission/submission.middleware.ts new file mode 100644 index 0000000000..0acfc5da8e --- /dev/null +++ b/src/app/modules/submission/submission.middleware.ts @@ -0,0 +1,49 @@ +import { RequestHandler } from 'express' +import { ParamsDictionary } from 'express-serve-static-core' + +import { createLoggerWithLabel } from '../../../config/logger' +import { WithAutoReplyEmailData } from '../../../types' + +import * as SubmissionService from './submission.service' +import { ProcessedFieldResponse } from './submission.types' + +const logger = createLoggerWithLabel(module) + +/** + * Sends email confirmations to form-fillers, for email fields which have + * email confirmation enabled. + * @param req Express request object + * @param res Express response object + */ +export const sendEmailConfirmations: RequestHandler< + ParamsDictionary, + unknown, + { parsedResponses: ProcessedFieldResponse[] } +> = async (req, res) => { + const { + form, + attachments, + autoReplyData, + submission, + } = req as WithAutoReplyEmailData + // Return the reply early to the submitter + res.json({ + message: 'Form submission successful.', + submissionId: submission.id, + }) + return SubmissionService.sendEmailConfirmations({ + form, + parsedResponses: req.body.parsedResponses, + submission, + attachments, + autoReplyData, + }).mapErr((error) => { + logger.error({ + message: 'Error while sending email confirmations', + meta: { + action: 'sendEmailAutoReplies', + }, + error, + }) + }) +} diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index c516094768..410d800dbb 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -1,6 +1,6 @@ import _ from 'lodash' import mongoose from 'mongoose' -import { err, errAsync, ok, Result, ResultAsync } from 'neverthrow' +import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../config/logger' import { @@ -8,12 +8,17 @@ import { getVisibleFieldIds, } from '../../../shared/util/logic' import { + EmailAutoReplyField, FieldResponse, + IAttachmentInfo, IFieldSchema, IFormDocument, + IPopulatedForm, + ISubmissionSchema, ResponseMode, } from '../../../types' import getSubmissionModel from '../../models/submission.server.model' +import MailService from '../../services/mail/mail.service' import { createQueryWithDateParam, isMalformedDate } from '../../utils/date' import { validateField } from '../../utils/field-validation' import { DatabaseError, MalformedParametersError } from '../core/core.errors' @@ -21,10 +26,11 @@ import { DatabaseError, MalformedParametersError } from '../core/core.errors' import { ConflictError, ProcessingError, + SendEmailConfirmationError, ValidateFieldError, } from './submission.errors' import { ProcessedFieldResponse } from './submission.types' -import { getModeFilter } from './submission.utils' +import { extractEmailConfirmationData, getModeFilter } from './submission.utils' const logger = createLoggerWithLabel(module) const SubmissionModel = getSubmissionModel(mongoose) @@ -208,3 +214,72 @@ export const getFormSubmissionsCount = ( }, ) } + +/** + * Sends email confirmation to form-fillers, for email fields with email confirmation + * enabled. + * @param param0 Data to include in email confirmations + * @param param0.form Form object + * @param param0.submission Submission object which was saved to database + * @param param0.parsedResponses Responses for each field + * @param param0.autoReplyData Subset of responses to be included in email confirmation + * @param param0.attachments Attachments to be included in email + * @returns ok(true) if all emails were sent successfully + * @returns err(SendEmailConfirmationError) if any email failed to be sent + */ +export const sendEmailConfirmations = ({ + form, + submission, + parsedResponses, + autoReplyData, + attachments, +}: { + form: IPopulatedForm + submission: ISubmissionSchema + parsedResponses: ProcessedFieldResponse[] + autoReplyData?: EmailAutoReplyField[] + attachments?: IAttachmentInfo[] +}): ResultAsync => { + const logMeta = { + action: 'sendEmailConfirmations', + formId: form._id, + submissionid: submission._id, + } + const confirmationData = extractEmailConfirmationData( + parsedResponses, + form.form_fields, + ) + if (confirmationData.length === 0) { + return okAsync(true) + } + const sentEmailsPromise = MailService.sendAutoReplyEmails({ + form, + submission, + attachments, + responsesData: autoReplyData ?? [], + autoReplyMailDatas: confirmationData, + }) + return ResultAsync.fromPromise(sentEmailsPromise, (error) => { + logger.error({ + message: 'Error while attempting to send email confirmations', + meta: logMeta, + error, + }) + return new SendEmailConfirmationError() + }).andThen((emailResults) => { + const errors = emailResults.reduce((acc, singleEmail) => { + if (singleEmail.status === 'rejected') { + acc.push(singleEmail.reason) + } + return acc + }, []) + if (errors.length > 0) { + logger.error({ + message: 'Some email confirmations could not be sent', + meta: { ...logMeta, errors }, + }) + return errAsync(new SendEmailConfirmationError()) + } + return okAsync(true) + }) +} diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 39aceea9cf..14e411f265 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -1,7 +1,12 @@ -import { BasicField, ResponseMode } from '../../../types' -import { assertUnreachable } from '../../utils/assert-unreachable' +import { keyBy } from 'lodash' + +import { BasicField, IFieldSchema, ResponseMode } from '../../../types' +import { isEmailField } from '../../../types/field/utils/guards' +import { AutoReplyMailData } from '../../services/mail/mail.types' import { FIELDS_TO_REJECT } from '../../utils/field-validation/config' +import { ProcessedFieldResponse } from './submission.types' + type ModeFilterParam = { fieldType: BasicField } @@ -14,8 +19,6 @@ export const getModeFilter = ( return emailModeFilter case ResponseMode.Encrypt: return encryptModeFilter - default: - return assertUnreachable(responseMode) } } @@ -31,3 +34,37 @@ const encryptModeFilter = (responses: T[] = []) => { [BasicField.Mobile, BasicField.Email].includes(fieldType), ) } + +/** + * Extracts response data to be sent in email confirmations + * @param parsedResponses Responses from form filler + * @param formFields Fields from form object + * @returns Array of data for email confirmations + */ +export const extractEmailConfirmationData = ( + parsedResponses: ProcessedFieldResponse[], + formFields: IFieldSchema[] | undefined, +): AutoReplyMailData[] => { + const fieldsById = keyBy(formFields, '_id') + return parsedResponses.reduce((acc, response) => { + const field = fieldsById[response._id] + if ( + field && + isEmailField(field) && + response.fieldType === BasicField.Email && + response.answer + ) { + const options = field.autoReplyOptions + if (options.hasAutoReply) { + acc.push({ + email: response.answer, + subject: options.autoReplySubject, + sender: options.autoReplySender, + body: options.autoReplyMessage, + includeFormSummary: options.includeFormSummary, + }) + } + } + return acc + }, []) +} diff --git a/src/app/modules/user/__tests__/user.service.spec.ts b/src/app/modules/user/__tests__/user.service.spec.ts index aa80f3df22..66f84e0824 100644 --- a/src/app/modules/user/__tests__/user.service.spec.ts +++ b/src/app/modules/user/__tests__/user.service.spec.ts @@ -1,4 +1,5 @@ import { ObjectID } from 'bson' +import { zipWith } from 'lodash' import MockDate from 'mockdate' import mongoose, { Query } from 'mongoose' import { errAsync, okAsync } from 'neverthrow' @@ -54,6 +55,40 @@ describe('user.service', () => { }) afterAll(async () => await dbHandler.closeDatabase()) + describe('findContactsForEmails', () => { + it('should call the correct model static method with the given emails', async () => { + const mockNumbers = ['+6581234567', '+6581234568'] + const mockEmails = ['a@abc.com', 'b@def.com'] + const mockContacts = zipWith( + mockEmails, + mockNumbers, + (email, contact) => ({ email, contact }), + ) + const findContactsSpy = jest + .spyOn(UserModel, 'findContactNumbersByEmails') + .mockResolvedValueOnce(mockContacts) + + const result = await UserService.findContactsForEmails(mockEmails) + + expect(findContactsSpy).toHaveBeenCalledWith(mockEmails) + expect(result._unsafeUnwrap()).toEqual(mockContacts) + }) + + it('should return DatabaseError when database query fails', async () => { + const mockEmails = ['a@abc.com', 'b@def.com'] + const findContactsSpy = jest + .spyOn(UserModel, 'findContactNumbersByEmails') + .mockRejectedValueOnce(new Error()) + + const result = await UserService.findContactsForEmails(mockEmails) + + expect(findContactsSpy).toHaveBeenCalledWith(mockEmails) + expect(result._unsafeUnwrapErr()).toEqual( + new DatabaseError('Failed to retrieve contacts for email addresses'), + ) + }) + }) + describe('createContactOtp', () => { it('should create a new AdminVerification document and return otp', async () => { // Arrange diff --git a/src/app/modules/user/user.service.ts b/src/app/modules/user/user.service.ts index 9b8b91a5cc..0fd304512f 100644 --- a/src/app/modules/user/user.service.ts +++ b/src/app/modules/user/user.service.ts @@ -10,6 +10,7 @@ import { IPopulatedUser, IUserSchema, UpsertOtpParams, + UserContactView, } from '../../../types' import getAdminVerificationModel from '../../models/admin_verification.server.model' import { AGENCY_SCHEMA_ID } from '../../models/agency.server.model' @@ -465,3 +466,29 @@ const removeAdminVerificationDoc = (userId: string) => { }, ) } + +/** + * Finds emergency contact numbers for a given list of emails. + * @param emails Emails for which contacts should be found + * @returns Array of email-contact pairings + */ +export const findContactsForEmails = ( + emails: string[], +): ResultAsync => { + return ResultAsync.fromPromise( + UserModel.findContactNumbersByEmails(emails), + (error) => { + logger.error({ + message: 'Error while retrieving contacts for email addresses', + meta: { + action: 'findContactsForEmails', + emails, + }, + error, + }) + return new DatabaseError( + 'Failed to retrieve contacts for email addresses', + ) + }, + ) +} diff --git a/src/app/modules/verification/verification.service.ts b/src/app/modules/verification/verification.service.ts index 5b9e6fb6ae..6a35d3a575 100644 --- a/src/app/modules/verification/verification.service.ts +++ b/src/app/modules/verification/verification.service.ts @@ -332,5 +332,7 @@ const getFieldFromTransaction = ( const throwError = (message: string, name?: string): never => { const error = new Error(message) error.name = name || message + // TODO(#941) Convert this service to use neverthrow, and re-examine type assertions made + // eslint-disable-next-line throw error } diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index 789d83db3f..22405c9866 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -8,8 +8,8 @@ const { celebrate, Joi, Segments } = require('celebrate') let forms = require('../../app/controllers/forms.server.controller') let adminForms = require('../../app/controllers/admin-forms.server.controller') let auth = require('../../app/controllers/authentication.server.controller') -let submissions = require('../../app/controllers/submissions.server.controller') const EmailSubmissionsMiddleware = require('../../app/modules/submission/email-submission/email-submission.middleware') +const SubmissionsMiddleware = require('../../app/modules/submission/submission.middleware') const webhookVerifiedContentFactory = require('../factories/webhook-verified-content.factory') const AdminFormController = require('../modules/form/admin-form/admin-form.controller') const { withUserAuthentication } = require('../modules/auth/auth.middlewares') @@ -447,12 +447,11 @@ module.exports = function (app) { }), EmailSubmissionsMiddleware.validateEmailSubmission, AdminFormController.passThroughSpcp, - submissions.injectAutoReplyInfo, SpcpController.appendVerifiedSPCPResponses, EmailSubmissionsMiddleware.prepareEmailSubmission, adminForms.passThroughSaveMetadataToDb, EmailSubmissionsMiddleware.sendAdminEmail, - submissions.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations, ) /** @@ -523,11 +522,10 @@ module.exports = function (app) { authActiveForm(PermissionLevel.Read), EncryptSubmissionMiddleware.validateAndProcessEncryptSubmission, AdminFormController.passThroughSpcp, - submissions.injectAutoReplyInfo, webhookVerifiedContentFactory.encryptedVerifiedFields, EncryptSubmissionMiddleware.prepareEncryptSubmission, adminForms.passThroughSaveMetadataToDb, - submissions.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations, ) /** diff --git a/src/app/routes/public-forms.server.routes.js b/src/app/routes/public-forms.server.routes.js index 2b92e352d9..4a3a57af81 100644 --- a/src/app/routes/public-forms.server.routes.js +++ b/src/app/routes/public-forms.server.routes.js @@ -5,13 +5,13 @@ */ const forms = require('../../app/controllers/forms.server.controller') const publicForms = require('../modules/form/public-form/public-form.middlewares') -const submissions = require('../../app/controllers/submissions.server.controller') const encryptSubmissions = require('../../app/controllers/encrypt-submissions.server.controller') const myInfoController = require('../../app/controllers/myinfo.server.controller') const { celebrate, Joi, Segments } = require('celebrate') const webhookVerifiedContentFactory = require('../factories/webhook-verified-content.factory') const { CaptchaFactory } = require('../services/captcha/captcha.factory') const CaptchaMiddleware = require('../services/captcha/captcha.middleware') +const SubmissionsMiddleware = require('../../app/modules/submission/submission.middleware') const { limitRate } = require('../utils/limit-rate') const { rateLimitConfig } = require('../../config/config') const PublicFormController = require('../modules/form/public-form/public-form.controller') @@ -199,12 +199,11 @@ module.exports = function (app) { }), EmailSubmissionsMiddleware.validateEmailSubmission, myInfoController.verifyMyInfoVals, - submissions.injectAutoReplyInfo, SpcpController.appendVerifiedSPCPResponses, EmailSubmissionsMiddleware.prepareEmailSubmission, EmailSubmissionsMiddleware.saveMetadataToDb, EmailSubmissionsMiddleware.sendAdminEmail, - submissions.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations, ) /** @@ -279,11 +278,10 @@ module.exports = function (app) { EncryptSubmissionMiddleware.validateAndProcessEncryptSubmission, SpcpController.isSpcpAuthenticated, myInfoController.verifyMyInfoVals, - submissions.injectAutoReplyInfo, webhookVerifiedContentFactory.encryptedVerifiedFields, EncryptSubmissionMiddleware.prepareEncryptSubmission, encryptSubmissions.saveResponseToDb, webhookVerifiedContentFactory.post, - submissions.sendAutoReply, + SubmissionsMiddleware.sendEmailConfirmations, ) } diff --git a/src/app/services/mail/mail.service.ts b/src/app/services/mail/mail.service.ts index 6bed20093e..4bb0f76c90 100644 --- a/src/app/services/mail/mail.service.ts +++ b/src/app/services/mail/mail.service.ts @@ -8,8 +8,12 @@ import validator from 'validator' import config from '../../../config/config' import { createLoggerWithLabel } from '../../../config/logger' import { HASH_EXPIRE_AFTER_SECONDS } from '../../../shared/util/verification' -import { BounceType, IEmailFormSchema, ISubmissionSchema } from '../../../types' -import { EmailFormField } from '../../modules/submission/email-submission/email-submission.types' +import { + BounceType, + EmailFormField, + IEmailFormSchema, + ISubmissionSchema, +} from '../../../types' import { EMAIL_HEADERS, EmailType } from './mail.constants' import { MailSendError } from './mail.errors' diff --git a/src/app/services/mail/mail.types.ts b/src/app/services/mail/mail.types.ts index 61a0592ab6..1089d29cbd 100644 --- a/src/app/services/mail/mail.types.ts +++ b/src/app/services/mail/mail.types.ts @@ -3,11 +3,11 @@ import { OperationOptions } from 'retry' import { AutoReplyOptions, + EmailFormField, IFormSchema, IPopulatedForm, ISubmissionSchema, } from '../../../types' -import { EmailFormField } from '../../modules/submission/email-submission/email-submission.types' export type SendMailOptions = { mailId?: string diff --git a/src/app/services/sms/__tests__/sms.factory.spec.ts b/src/app/services/sms/__tests__/sms.factory.spec.ts index 5e7952f8bd..930fce6d36 100644 --- a/src/app/services/sms/__tests__/sms.factory.spec.ts +++ b/src/app/services/sms/__tests__/sms.factory.spec.ts @@ -1,6 +1,9 @@ +import { ObjectId } from 'bson' import { okAsync } from 'neverthrow' +import { mocked } from 'ts-jest/utils' import Twilio from 'twilio' +import { MissingFeatureError } from 'src/app/modules/core/core.errors' import { FeatureNames, ISms, @@ -9,7 +12,7 @@ import { import { createSmsFactory } from '../sms.factory' import * as SmsService from '../sms.service' -import { TwilioConfig } from '../sms.types' +import { BounceNotificationSmsParams, TwilioConfig } from '../sms.types' // This is hoisted and thus a const cannot be passed in. jest.mock('twilio', () => @@ -18,10 +21,22 @@ jest.mock('twilio', () => })), ) +jest.mock('../sms.service') +const MockSmsService = mocked(SmsService, true) + const MOCKED_TWILIO = ({ mocked: 'this is mocked', } as unknown) as Twilio.Twilio +const MOCK_BOUNCE_SMS_PARAMS: BounceNotificationSmsParams = { + adminEmail: 'admin@email.com', + adminId: new ObjectId().toHexString(), + formId: new ObjectId().toHexString(), + formTitle: 'mock form title', + recipient: '+6581234567', + recipientEmail: 'recipient@email.com', +} + describe('sms.factory', () => { beforeEach(() => jest.clearAllMocks()) @@ -54,6 +69,18 @@ describe('sms.factory', () => { 'sendVerificationOtp: SMS feature must be enabled in Feature Manager first', ) }) + + it('should reject with MissingFeatureError for sendFormDeactivatedSms', async () => { + await expect( + SmsFactory.sendFormDeactivatedSms(MOCK_BOUNCE_SMS_PARAMS), + ).rejects.toThrowError(new MissingFeatureError(FeatureNames.Sms)) + }) + + it('should reject with MissingFeatureError for sendBouncedSubmissionSms', async () => { + await expect( + SmsFactory.sendBouncedSubmissionSms(MOCK_BOUNCE_SMS_PARAMS), + ).rejects.toThrowError(new MissingFeatureError(FeatureNames.Sms)) + }) }) describe('sms feature enabled', () => { @@ -68,14 +95,15 @@ describe('sms.factory', () => { twilioMsgSrvcSid: 'formsg-is-great-pleasehelpme', }, } - + const expectedTwilioConfig: TwilioConfig = { + msgSrvcSid: MOCK_ENABLED_SMS_FEATURE.props.twilioMsgSrvcSid, + client: MOCKED_TWILIO, + } const SmsFactory = createSmsFactory(MOCK_ENABLED_SMS_FEATURE) it('should call SmsService counterpart when invoking sendAdminContactOtp', async () => { // Arrange - const serviceContactSpy = jest - .spyOn(SmsService, 'sendAdminContactOtp') - .mockReturnValueOnce(okAsync(true)) + MockSmsService.sendAdminContactOtp.mockReturnValueOnce(okAsync(true)) const mockArguments: Parameters = [ 'mockRecipient', @@ -87,13 +115,8 @@ describe('sms.factory', () => { await SmsFactory.sendAdminContactOtp(...mockArguments) // Assert - const expectedTwilioConfig: TwilioConfig = { - msgSrvcSid: MOCK_ENABLED_SMS_FEATURE.props.twilioMsgSrvcSid, - client: MOCKED_TWILIO, - } - - expect(serviceContactSpy).toHaveBeenCalledTimes(1) - expect(serviceContactSpy).toHaveBeenCalledWith( + expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledTimes(1) + expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledWith( ...mockArguments, expectedTwilioConfig, ) @@ -101,9 +124,7 @@ describe('sms.factory', () => { it('should call SmsService counterpart when invoking sendVerificationOtp', async () => { // Arrange - const serviceVfnSpy = jest - .spyOn(SmsService, 'sendVerificationOtp') - .mockResolvedValue(true) + MockSmsService.sendVerificationOtp.mockResolvedValue(true) const mockArguments: Parameters = [ 'mockRecipient', @@ -115,16 +136,33 @@ describe('sms.factory', () => { await SmsFactory.sendVerificationOtp(...mockArguments) // Assert - const expectedTwilioConfig: TwilioConfig = { - msgSrvcSid: MOCK_ENABLED_SMS_FEATURE.props.twilioMsgSrvcSid, - client: MOCKED_TWILIO, - } - - expect(serviceVfnSpy).toHaveBeenCalledTimes(1) - expect(serviceVfnSpy).toHaveBeenCalledWith( + expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledTimes(1) + expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledWith( ...mockArguments, expectedTwilioConfig, ) }) + + it('should call SmsService when sendFormDeactivatedSms is called', async () => { + MockSmsService.sendFormDeactivatedSms.mockResolvedValueOnce(true) + + await SmsFactory.sendFormDeactivatedSms(MOCK_BOUNCE_SMS_PARAMS) + + expect(MockSmsService.sendFormDeactivatedSms).toHaveBeenCalledWith( + MOCK_BOUNCE_SMS_PARAMS, + expectedTwilioConfig, + ) + }) + + it('should call SmsService when sendBouncedSubmissionSms is called', async () => { + MockSmsService.sendBouncedSubmissionSms.mockResolvedValueOnce(true) + + await SmsFactory.sendBouncedSubmissionSms(MOCK_BOUNCE_SMS_PARAMS) + + expect(MockSmsService.sendBouncedSubmissionSms).toHaveBeenCalledWith( + MOCK_BOUNCE_SMS_PARAMS, + expectedTwilioConfig, + ) + }) }) }) diff --git a/src/app/services/sms/__tests__/sms.service.spec.ts b/src/app/services/sms/__tests__/sms.service.spec.ts index d69a67d93f..378d2a03bc 100644 --- a/src/app/services/sms/__tests__/sms.service.spec.ts +++ b/src/app/services/sms/__tests__/sms.service.spec.ts @@ -1,3 +1,4 @@ +import { ObjectId } from 'bson' import mongoose from 'mongoose' import getFormModel from 'src/app/models/form.server.model' @@ -8,6 +9,10 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db' import * as SmsService from '../sms.service' import { LogType, SmsType, TwilioConfig } from '../sms.types' +import { + renderBouncedSubmissionSms, + renderFormDeactivatedSms, +} from '../sms.util' import getSmsCountModel from '../sms_count.server.model' const FormModel = getFormModel(mongoose) @@ -16,30 +21,38 @@ const SmsCountModel = getSmsCountModel(mongoose) // Test numbers provided by Twilio: // https://www.twilio.com/docs/iam/test-credentials const TWILIO_TEST_NUMBER = '+15005550006' - const MOCK_MSG_SRVC_SID = 'mockMsgSrvcSid' +const MOCK_RECIPIENT_EMAIL = 'recipientEmail@email.com' +const MOCK_ADMIN_EMAIL = 'adminEmail@email.com' +const MOCK_ADMIN_ID = new ObjectId().toHexString() +const MOCK_FORM_ID = new ObjectId().toHexString() +const MOCK_FORM_TITLE = 'formTitle' + +const twilioSuccessSpy = jest.fn().mockResolvedValue({ + status: 'testStatus', + sid: 'testSid', +}) const MOCK_VALID_CONFIG = ({ msgSrvcSid: MOCK_MSG_SRVC_SID, client: { messages: { - create: jest.fn().mockResolvedValue({ - status: 'testStatus', - sid: 'testSid', - }), + create: twilioSuccessSpy, }, }, } as unknown) as TwilioConfig +const twilioFailureSpy = jest.fn().mockResolvedValue({ + status: 'testStatus', + sid: undefined, + errorCode: 21211, +}) + const MOCK_INVALID_CONFIG = ({ msgSrvcSid: MOCK_MSG_SRVC_SID, client: { messages: { - create: jest.fn().mockResolvedValue({ - status: 'testStatus', - sid: undefined, - errorCode: 21211, - }), + create: twilioFailureSpy, }, }, } as unknown) as TwilioConfig @@ -53,11 +66,169 @@ describe('sms.service', () => { beforeEach(async () => { const { user } = await dbHandler.insertFormCollectionReqs() testUser = user - smsCountSpy.mockClear() + jest.clearAllMocks() }) afterEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) + describe('sendFormDeactivatedSms', () => { + it('should send SMS and log success when sending is successful', async () => { + const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) + + await SmsService.sendFormDeactivatedSms( + { + recipient: TWILIO_TEST_NUMBER, + adminEmail: MOCK_ADMIN_EMAIL, + adminId: MOCK_ADMIN_ID, + formId: MOCK_FORM_ID, + formTitle: MOCK_FORM_TITLE, + recipientEmail: MOCK_RECIPIENT_EMAIL, + }, + MOCK_VALID_CONFIG, + ) + + expect(twilioSuccessSpy).toHaveBeenCalledWith({ + to: TWILIO_TEST_NUMBER, + body: expectedMessage, + from: MOCK_VALID_CONFIG.msgSrvcSid, + forceDelivery: true, + }) + expect(smsCountSpy).toHaveBeenCalledWith({ + smsData: { + form: MOCK_FORM_ID, + collaboratorEmail: MOCK_RECIPIENT_EMAIL, + recipientNumber: TWILIO_TEST_NUMBER, + formAdmin: { + email: MOCK_ADMIN_EMAIL, + userId: MOCK_ADMIN_ID, + }, + }, + smsType: SmsType.DeactivatedForm, + msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, + logType: LogType.success, + }) + }) + + it('should log failure when sending fails', async () => { + const expectedMessage = renderFormDeactivatedSms(MOCK_FORM_TITLE) + const expectedError = new Error(VfnErrors.InvalidMobileNumber) + expectedError.name = VfnErrors.SendOtpFailed + + const resultPromise = SmsService.sendFormDeactivatedSms( + { + recipient: TWILIO_TEST_NUMBER, + adminEmail: MOCK_ADMIN_EMAIL, + adminId: MOCK_ADMIN_ID, + formId: MOCK_FORM_ID, + formTitle: MOCK_FORM_TITLE, + recipientEmail: MOCK_RECIPIENT_EMAIL, + }, + MOCK_INVALID_CONFIG, + ) + + await expect(resultPromise).rejects.toThrowError(expectedError) + expect(twilioFailureSpy).toHaveBeenCalledWith({ + to: TWILIO_TEST_NUMBER, + body: expectedMessage, + from: MOCK_INVALID_CONFIG.msgSrvcSid, + forceDelivery: true, + }) + expect(smsCountSpy).toHaveBeenCalledWith({ + smsData: { + form: MOCK_FORM_ID, + collaboratorEmail: MOCK_RECIPIENT_EMAIL, + recipientNumber: TWILIO_TEST_NUMBER, + formAdmin: { + email: MOCK_ADMIN_EMAIL, + userId: MOCK_ADMIN_ID, + }, + }, + smsType: SmsType.DeactivatedForm, + msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, + logType: LogType.failure, + }) + }) + }) + + describe('sendBouncedSubmissionSms', () => { + it('should send SMS and log success when sending is successful', async () => { + const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) + + await SmsService.sendBouncedSubmissionSms( + { + recipient: TWILIO_TEST_NUMBER, + adminEmail: MOCK_ADMIN_EMAIL, + adminId: MOCK_ADMIN_ID, + formId: MOCK_FORM_ID, + formTitle: MOCK_FORM_TITLE, + recipientEmail: MOCK_RECIPIENT_EMAIL, + }, + MOCK_VALID_CONFIG, + ) + + expect(twilioSuccessSpy).toHaveBeenCalledWith({ + to: TWILIO_TEST_NUMBER, + body: expectedMessage, + from: MOCK_VALID_CONFIG.msgSrvcSid, + forceDelivery: true, + }) + expect(smsCountSpy).toHaveBeenCalledWith({ + smsData: { + form: MOCK_FORM_ID, + collaboratorEmail: MOCK_RECIPIENT_EMAIL, + recipientNumber: TWILIO_TEST_NUMBER, + formAdmin: { + email: MOCK_ADMIN_EMAIL, + userId: MOCK_ADMIN_ID, + }, + }, + smsType: SmsType.BouncedSubmission, + msgSrvcSid: MOCK_VALID_CONFIG.msgSrvcSid, + logType: LogType.success, + }) + }) + + it('should log failure when sending fails', async () => { + const expectedMessage = renderBouncedSubmissionSms(MOCK_FORM_TITLE) + const expectedError = new Error(VfnErrors.InvalidMobileNumber) + expectedError.name = VfnErrors.SendOtpFailed + + const resultPromise = SmsService.sendBouncedSubmissionSms( + { + recipient: TWILIO_TEST_NUMBER, + adminEmail: MOCK_ADMIN_EMAIL, + adminId: MOCK_ADMIN_ID, + formId: MOCK_FORM_ID, + formTitle: MOCK_FORM_TITLE, + recipientEmail: MOCK_RECIPIENT_EMAIL, + }, + MOCK_INVALID_CONFIG, + ) + + await expect(resultPromise).rejects.toThrowError(expectedError) + expect(twilioFailureSpy).toHaveBeenCalledWith({ + to: TWILIO_TEST_NUMBER, + body: expectedMessage, + from: MOCK_INVALID_CONFIG.msgSrvcSid, + forceDelivery: true, + }) + expect(smsCountSpy).toHaveBeenCalledWith({ + smsData: { + form: MOCK_FORM_ID, + collaboratorEmail: MOCK_RECIPIENT_EMAIL, + recipientNumber: TWILIO_TEST_NUMBER, + formAdmin: { + email: MOCK_ADMIN_EMAIL, + userId: MOCK_ADMIN_ID, + }, + }, + smsType: SmsType.BouncedSubmission, + msgSrvcSid: MOCK_INVALID_CONFIG.msgSrvcSid, + logType: LogType.failure, + }) + }) + }) + describe('sendVerificationOtp', () => { let mockOtpData: FormOtpData let testForm: IFormSchema @@ -112,9 +283,9 @@ describe('sms.service', () => { await expect(actualPromise).resolves.toEqual(true) // Logging should also have happened. const expectedLogParams = { - otpData: mockOtpData, + smsData: mockOtpData, msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.verification, + smsType: SmsType.Verification, logType: LogType.success, } expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) @@ -139,9 +310,9 @@ describe('sms.service', () => { await expect(actualPromise).rejects.toThrow(expectedError) // Logging should also have happened. const expectedLogParams = { - otpData: mockOtpData, + smsData: mockOtpData, msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.verification, + smsType: SmsType.Verification, logType: LogType.failure, } expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) @@ -164,11 +335,11 @@ describe('sms.service', () => { expect(actualResult._unsafeUnwrap()).toEqual(true) // Logging should also have happened. const expectedLogParams = { - otpData: { + smsData: { admin: testUser._id, }, msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.adminContact, + smsType: SmsType.AdminContact, logType: LogType.success, } expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) @@ -192,11 +363,11 @@ describe('sms.service', () => { // Logging should also have happened. const expectedLogParams = { - otpData: { + smsData: { admin: testUser._id, }, msgSrvcSid: MOCK_MSG_SRVC_SID, - smsType: SmsType.adminContact, + smsType: SmsType.AdminContact, logType: LogType.failure, } expect(smsCountSpy).toHaveBeenCalledWith(expectedLogParams) diff --git a/src/app/services/sms/__tests__/sms_count.server.model.spec.ts b/src/app/services/sms/__tests__/sms_count.server.model.spec.ts index abc3c49a75..7a022c6f86 100644 --- a/src/app/services/sms/__tests__/sms_count.server.model.spec.ts +++ b/src/app/services/sms/__tests__/sms_count.server.model.spec.ts @@ -18,6 +18,32 @@ const MOCK_SMSCOUNT_PARAMS = { }, } +const MOCK_BOUNCED_SUBMISSION_PARAMS = { + form: new ObjectId(), + formAdmin: { + email: 'a@abc.com', + userId: new ObjectId(), + }, + collaboratorEmail: 'b@def.com', + recipientNumber: '+6581234567', + msgSrvcSid: 'mockMsgSrvcSid', + smsType: SmsType.BouncedSubmission, + logType: LogType.success, +} + +const MOCK_FORM_DEACTIVATED_PARAMS = { + form: new ObjectId(), + formAdmin: { + email: 'a@abc.com', + userId: new ObjectId(), + }, + collaboratorEmail: 'b@def.com', + recipientNumber: '+6581234567', + msgSrvcSid: 'mockMsgSrvcSid', + smsType: SmsType.DeactivatedForm, + logType: LogType.success, +} + const MOCK_MSG_SRVC_SID = 'mockMsgSrvcSid' describe('SmsCount', () => { @@ -25,6 +51,184 @@ describe('SmsCount', () => { beforeEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) + describe('FormDeactivatedSmsCountSchema', () => { + it('should create and save successfully', async () => { + const saved = await SmsCount.create(MOCK_FORM_DEACTIVATED_PARAMS) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual(MOCK_FORM_DEACTIVATED_PARAMS) + }) + + it('should reject if form is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_FORM_DEACTIVATED_PARAMS, 'form'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if formAdmin.email is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_FORM_DEACTIVATED_PARAMS, 'formAdmin.email'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if formAdmin.userId is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_FORM_DEACTIVATED_PARAMS, 'formAdmin.userId'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if collaboratorEmail is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_FORM_DEACTIVATED_PARAMS, 'collaboratorEmail'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if collaboratorEmail is invalid', async () => { + const invalidSmsCount = new SmsCount( + merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { + collaboratorEmail: 'invalid', + }), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if recipientNumber is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_FORM_DEACTIVATED_PARAMS, 'recipientNumber'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if recipientNumber is invalid', async () => { + const invalidSmsCount = new SmsCount( + merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { + recipientNumber: 'invalid', + }), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + }) + + describe('BouncedSubmissionSmsCountSchema', () => { + it('should create and save successfully', async () => { + const saved = await SmsCount.create(MOCK_BOUNCED_SUBMISSION_PARAMS) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual(MOCK_BOUNCED_SUBMISSION_PARAMS) + }) + + it('should reject if form is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_BOUNCED_SUBMISSION_PARAMS, 'form'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if formAdmin.email is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_BOUNCED_SUBMISSION_PARAMS, 'formAdmin.email'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if formAdmin.userId is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_BOUNCED_SUBMISSION_PARAMS, 'formAdmin.userId'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if collaboratorEmail is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_BOUNCED_SUBMISSION_PARAMS, 'collaboratorEmail'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if collaboratorEmail is invalid', async () => { + const invalidSmsCount = new SmsCount( + merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { + collaboratorEmail: 'invalid', + }), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if recipientNumber is missing', async () => { + const invalidSmsCount = new SmsCount( + omit(MOCK_BOUNCED_SUBMISSION_PARAMS, 'recipientNumber'), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should reject if recipientNumber is invalid', async () => { + const invalidSmsCount = new SmsCount( + merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { + recipientNumber: 'invalid', + }), + ) + + await expect(invalidSmsCount.save()).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + }) + describe('VerificationCount Schema', () => { it('should create and save successfully', async () => { // Arrange @@ -175,13 +379,113 @@ describe('SmsCount', () => { describe('logSms', () => { const MOCK_FORM_ID = MOCK_SMSCOUNT_PARAMS.form + it('should correctly log bounced submission SMS successes', async () => { + const saved = await SmsCount.logSms({ + logType: LogType.success, + smsType: SmsType.BouncedSubmission, + msgSrvcSid: MOCK_BOUNCED_SUBMISSION_PARAMS.msgSrvcSid, + smsData: omit(MOCK_BOUNCED_SUBMISSION_PARAMS, [ + 'msgSrvcSid', + 'smsType', + 'logType', + ]), + }) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual( + merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { + logType: LogType.success, + }), + ) + }) + + it('should correctly log bounced submission SMS failures', async () => { + const saved = await SmsCount.logSms({ + logType: LogType.failure, + smsType: SmsType.BouncedSubmission, + msgSrvcSid: MOCK_BOUNCED_SUBMISSION_PARAMS.msgSrvcSid, + smsData: omit(MOCK_BOUNCED_SUBMISSION_PARAMS, [ + 'msgSrvcSid', + 'smsType', + 'logType', + ]), + }) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual( + merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { + logType: LogType.failure, + }), + ) + }) + + it('should correctly log form deactivated SMS successes', async () => { + const saved = await SmsCount.logSms({ + logType: LogType.success, + smsType: SmsType.DeactivatedForm, + msgSrvcSid: MOCK_FORM_DEACTIVATED_PARAMS.msgSrvcSid, + smsData: omit(MOCK_FORM_DEACTIVATED_PARAMS, [ + 'msgSrvcSid', + 'smsType', + 'logType', + ]), + }) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual( + merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { logType: LogType.success }), + ) + }) + + it('should correctly log form deactivated SMS failures', async () => { + const saved = await SmsCount.logSms({ + logType: LogType.failure, + smsType: SmsType.DeactivatedForm, + msgSrvcSid: MOCK_FORM_DEACTIVATED_PARAMS.msgSrvcSid, + smsData: omit(MOCK_FORM_DEACTIVATED_PARAMS, [ + 'msgSrvcSid', + 'smsType', + 'logType', + ]), + }) + + expect(saved._id).toBeDefined() + expect(saved.createdAt).toBeInstanceOf(Date) + const actualSavedObject = omit(saved.toObject(), [ + '_id', + 'createdAt', + '__v', + ]) + expect(actualSavedObject).toEqual( + merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { logType: LogType.failure }), + ) + }) + it('should successfully log verification successes in the collection', async () => { // Arrange const initialCount = await SmsCount.countDocuments({}) // Act const expectedLog = await logAndReturnExpectedLog({ - smsType: SmsType.verification, + smsType: SmsType.Verification, logType: LogType.success, }) @@ -207,7 +511,7 @@ describe('SmsCount', () => { // Act const expectedLog = await logAndReturnExpectedLog({ - smsType: SmsType.verification, + smsType: SmsType.Verification, logType: LogType.failure, }) @@ -240,7 +544,7 @@ describe('SmsCount', () => { it('should reject if logType is invalid', async () => { await expect( logAndReturnExpectedLog({ - smsType: SmsType.verification, + smsType: SmsType.Verification, // @ts-ignore logType: 'INVALID', }), @@ -252,7 +556,7 @@ describe('SmsCount', () => { const createVerificationSmsCountParams = ({ logType = LogType.success, - smsType = SmsType.verification, + smsType = SmsType.Verification, }: { logType?: LogType smsType?: SmsType @@ -275,7 +579,7 @@ const logAndReturnExpectedLog = async ({ smsType: SmsType }) => { await SmsCount.logSms({ - otpData: MOCK_SMSCOUNT_PARAMS, + smsData: MOCK_SMSCOUNT_PARAMS, msgSrvcSid: MOCK_MSG_SRVC_SID, smsType, logType, diff --git a/src/app/services/sms/sms.factory.ts b/src/app/services/sms/sms.factory.ts index b1a70b130f..2330c4976e 100644 --- a/src/app/services/sms/sms.factory.ts +++ b/src/app/services/sms/sms.factory.ts @@ -4,9 +4,15 @@ import FeatureManager, { FeatureNames, RegisteredFeature, } from '../../../config/feature-manager' +import { MissingFeatureError } from '../../modules/core/core.errors' -import { sendAdminContactOtp, sendVerificationOtp } from './sms.service' -import { TwilioConfig } from './sms.types' +import { + sendAdminContactOtp, + sendBouncedSubmissionSms, + sendFormDeactivatedSms, + sendVerificationOtp, +} from './sms.service' +import { BounceNotificationSmsParams, TwilioConfig } from './sms.types' interface ISmsFactory { sendVerificationOtp: ( @@ -19,6 +25,34 @@ interface ISmsFactory { otp: string, userId: string, ) => ReturnType + /** + * Informs recipient that the given form was deactivated. Rejects if SMS feature + * not activated in app. + * @param params Data for SMS to be sent + * @param params.recipient Mobile number to be SMSed + * @param params.recipientEmail The email address of the recipient being SMSed + * @param params.adminId User ID of the admin of the deactivated form + * @param params.adminEmail Email of the admin of the deactivated form + * @param params.formId Form ID of deactivated form + * @param params.formTitle Title of deactivated form + */ + sendFormDeactivatedSms: ( + params: BounceNotificationSmsParams, + ) => ReturnType + /** + * Informs recipient that a response for the given form was lost due to email bounces. + * Rejects if SMS feature not activated in app. + * @param params Data for SMS to be sent + * @param params.recipient Mobile number to be SMSed + * @param params.recipientEmail The email address of the recipient being SMSed + * @param params.adminId User ID of the admin of the form + * @param params.adminEmail Email of the admin of the form + * @param params.formId Form ID of form + * @param params.formTitle Title of form + */ + sendBouncedSubmissionSms: ( + params: BounceNotificationSmsParams, + ) => ReturnType } const smsFeature = FeatureManager.get(FeatureNames.Sms) @@ -31,11 +65,17 @@ export const createSmsFactory = ( const errorMessage = 'SMS feature must be enabled in Feature Manager first' return { sendAdminContactOtp: () => { + //eslint-disable-next-line throw new Error(`sendAdminContactOtp: ${errorMessage}`) }, sendVerificationOtp: () => { + //eslint-disable-next-line throw new Error(`sendVerificationOtp: ${errorMessage}`) }, + sendFormDeactivatedSms: () => + Promise.reject(new MissingFeatureError(FeatureNames.Sms)), + sendBouncedSubmissionSms: () => + Promise.reject(new MissingFeatureError(FeatureNames.Sms)), } } @@ -59,6 +99,10 @@ export const createSmsFactory = ( sendVerificationOtp(recipient, otp, formId, twilioConfig), sendAdminContactOtp: (recipient, otp, userId) => sendAdminContactOtp(recipient, otp, userId, twilioConfig), + sendFormDeactivatedSms: (params) => + sendFormDeactivatedSms(params, twilioConfig), + sendBouncedSubmissionSms: (params) => + sendBouncedSubmissionSms(params, twilioConfig), } } diff --git a/src/app/services/sms/sms.service.ts b/src/app/services/sms/sms.service.ts index fde7ac97b0..19eb5b45fe 100644 --- a/src/app/services/sms/sms.service.ts +++ b/src/app/services/sms/sms.service.ts @@ -13,12 +13,19 @@ import getFormModel from '../../models/form.server.model' import { SmsSendError } from './sms.errors' import { + BouncedSubmissionSmsData, + BounceNotificationSmsParams, + FormDeactivatedSmsData, LogSmsParams, LogType, SmsType, TwilioConfig, TwilioCredentials, } from './sms.types' +import { + renderBouncedSubmissionSms, + renderFormDeactivatedSms, +} from './sms.util' import getSmsCountModel from './sms_count.server.model' const logger = createLoggerWithLabel(module) @@ -132,13 +139,13 @@ const getTwilio = async ( * @param twilioConfig The configuration used to send OTPs with * @param twilioData.client The client to use * @param twilioData.msgSrvcSid The message service sid to send from with. - * @param otpData The data for logging smsCount + * @param smsData The data for logging smsCount * @param recipient The mobile number of the recipient * @param message The message to send */ const send = async ( twilioConfig: TwilioConfig, - otpData: FormOtpData | AdminContactOtpData, + smsData: FormOtpData | AdminContactOtpData, recipient: string, message: string, smsType: SmsType, @@ -165,7 +172,7 @@ const send = async ( // Log success const logParams: LogSmsParams = { - otpData, + smsData, smsType, msgSrvcSid, logType: LogType.success, @@ -186,7 +193,7 @@ const send = async ( message: 'Successfully sent sms', meta: { action: 'send', - otpData, + smsData, }, }) @@ -203,7 +210,7 @@ const send = async ( // Log failure const logParams: LogSmsParams = { - otpData, + smsData, smsType, msgSrvcSid, logType: LogType.failure, @@ -269,7 +276,7 @@ export const sendVerificationOtp = async ( const message = `Use the OTP ${otp} to complete your submission on ${config.app.title}.` - return send(twilioData, otpData, recipient, message, SmsType.verification) + return send(twilioData, otpData, recipient, message, SmsType.Verification) } export const sendAdminContactOtp = ( @@ -293,7 +300,7 @@ export const sendAdminContactOtp = ( } return ResultAsync.fromPromise( - send(defaultConfig, otpData, recipient, message, SmsType.adminContact), + send(defaultConfig, otpData, recipient, message, SmsType.AdminContact), (error) => { logger.error({ message: 'Failed to send OTP for admin contact verification', @@ -318,3 +325,105 @@ export const sendAdminContactOtp = ( }, ) } + +/** + * Informs recipient that the given form was deactivated. + * @param params Data for SMS to be sent + * @param params.recipient Mobile number to be SMSed + * @param params.recipientEmail The email address of the recipient being SMSed + * @param params.adminId User ID of the admin of the deactivated form + * @param params.adminEmail Email of the admin of the deactivated form + * @param params.formId Form ID of deactivated form + * @param params.formTitle Title of deactivated form + * @param defaultConfig Twilio configuration + */ +export const sendFormDeactivatedSms = ( + { + recipient, + recipientEmail, + adminId, + adminEmail, + formId, + formTitle, + }: BounceNotificationSmsParams, + defaultConfig: TwilioConfig, +): Promise => { + logger.info({ + message: `Sending form deactivation notification for ${recipientEmail}`, + meta: { + action: 'sendFormDeactivatedSms', + formId, + }, + }) + + const message = renderFormDeactivatedSms(formTitle) + + const smsData: FormDeactivatedSmsData = { + form: formId, + collaboratorEmail: recipientEmail, + recipientNumber: recipient, + formAdmin: { + email: adminEmail, + userId: adminId, + }, + } + + return send( + defaultConfig, + smsData, + recipient, + message, + SmsType.DeactivatedForm, + ) +} + +/** + * Informs recipient that a response for the given form was lost due to email bounces. + * @param params Data for SMS to be sent + * @param params.recipient Mobile number to be SMSed + * @param params.recipientEmail The email address of the recipient being SMSed + * @param params.adminId User ID of the admin of the form + * @param params.adminEmail Email of the admin of the form + * @param params.formId Form ID of form + * @param params.formTitle Title of form + * @param defaultConfig Twilio configuration + */ +export const sendBouncedSubmissionSms = ( + { + recipient, + recipientEmail, + adminId, + adminEmail, + formId, + formTitle, + }: BounceNotificationSmsParams, + defaultConfig: TwilioConfig, +): Promise => { + logger.info({ + message: `Sending bounced submission notification for ${recipientEmail}`, + meta: { + action: 'sendBouncedSubmissionSms', + formId, + }, + }) + + const message = renderBouncedSubmissionSms(formTitle) + + const smsData: BouncedSubmissionSmsData = { + form: formId, + collaboratorEmail: recipientEmail, + recipientNumber: recipient, + formAdmin: { + email: adminEmail, + userId: adminId, + }, + } + + return send( + defaultConfig, + smsData, + recipient, + message, + SmsType.BouncedSubmission, + ) +} diff --git a/src/app/services/sms/sms.types.ts b/src/app/services/sms/sms.types.ts index 15552fb4e1..438837e53f 100644 --- a/src/app/services/sms/sms.types.ts +++ b/src/app/services/sms/sms.types.ts @@ -6,11 +6,14 @@ import { FormOtpData, IFormSchema, IUserSchema, -} from 'src/types' + Permission, +} from '../../../types' export enum SmsType { - verification = 'VERIFICATION', - adminContact = 'ADMIN_CONTACT', + Verification = 'VERIFICATION', + AdminContact = 'ADMIN_CONTACT', + DeactivatedForm = 'DEACTIVATED_FORM', + BouncedSubmission = 'BOUNCED_SUBMISSION', } export enum LogType { @@ -18,8 +21,24 @@ export enum LogType { success = 'SUCCESS', } +export type FormDeactivatedSmsData = { + form: IFormSchema['_id'] + formAdmin: { + email: IUserSchema['email'] + userId: IUserSchema['_id'] + } + collaboratorEmail: Permission['email'] + recipientNumber: string +} + +export type BouncedSubmissionSmsData = FormDeactivatedSmsData + export type LogSmsParams = { - otpData: FormOtpData | AdminContactOtpData + smsData: + | FormOtpData + | AdminContactOtpData + | FormDeactivatedSmsData + | BouncedSubmissionSmsData msgSrvcSid: string smsType: SmsType logType: LogType @@ -51,6 +70,22 @@ export interface IAdminContactSmsCount extends ISmsCount { export type IAdminContactSmsCountSchema = ISmsCountSchema +export interface IFormDeactivatedSmsCount + extends ISmsCount, + FormDeactivatedSmsData {} + +export interface IFormDeactivatedSmsCountSchema + extends ISmsCountSchema, + FormDeactivatedSmsData {} + +export interface IBouncedSubmissionSmsCount + extends ISmsCount, + BouncedSubmissionSmsData {} + +export interface IBouncedSubmissionSmsCountSchema + extends ISmsCountSchema, + BouncedSubmissionSmsData {} + export interface ISmsCountModel extends Model { logSms: (logParams: LogSmsParams) => Promise } @@ -66,3 +101,12 @@ export type TwilioConfig = { client: Twilio msgSrvcSid: string } + +export interface BounceNotificationSmsParams { + recipient: string + recipientEmail: string + adminId: string + adminEmail: string + formId: string + formTitle: string +} diff --git a/src/app/services/sms/sms.util.ts b/src/app/services/sms/sms.util.ts new file mode 100644 index 0000000000..9be7b77d2c --- /dev/null +++ b/src/app/services/sms/sms.util.ts @@ -0,0 +1,13 @@ +import dedent from 'dedent-js' + +export const renderFormDeactivatedSms = (formTitle: string): string => dedent` + Due to responses bouncing from all recipient inboxes, your form "${formTitle}" has been automatically deactivated to prevent further response loss. + + Please ensure your recipient email addresses (Settings tab) have the ability to receive emailed responses from us. Invalid email addresses should be deleted, and full inboxes should be cleared. + + If a systemic email issue is affecting email delivery, consider temporarily deactivating your form until email delivery is stable, or switching the form to Storage mode to continue receiving responses. +` + +export const renderBouncedSubmissionSms = (formTitle: string): string => dedent` + A response to your form "${formTitle}" has bounced from all recipient inboxes. Bounced responses cannot be recovered. To prevent more bounces, please ensure recipient email addresses are correct, and clear any full inboxes. +` diff --git a/src/app/services/sms/sms_count.server.model.ts b/src/app/services/sms/sms_count.server.model.ts index c0e2299d96..ea27658a2f 100644 --- a/src/app/services/sms/sms_count.server.model.ts +++ b/src/app/services/sms/sms_count.server.model.ts @@ -1,10 +1,14 @@ +import { parsePhoneNumberFromString } from 'libphonenumber-js/mobile' import { Mongoose, Schema } from 'mongoose' +import validator from 'validator' import { FORM_SCHEMA_ID } from '../../models/form.server.model' import { USER_SCHEMA_ID } from '../../models/user.server.model' import { IAdminContactSmsCountSchema, + IBouncedSubmissionSmsCountSchema, + IFormDeactivatedSmsCountSchema, ISmsCount, ISmsCountModel, ISmsCountSchema, @@ -40,6 +44,44 @@ const AdminContactSmsCountSchema = new Schema({ }, }) +const bounceSmsCountSchema = { + form: { + type: Schema.Types.ObjectId, + ref: FORM_SCHEMA_ID, + required: true, + }, + formAdmin: { + email: { type: String, required: true }, + userId: { + type: Schema.Types.ObjectId, + ref: USER_SCHEMA_ID, + required: true, + }, + }, + collaboratorEmail: { + type: String, + validate: validator.isEmail, + required: true, + }, + recipientNumber: { + type: String, + validate: (value: string) => { + const phoneNumber = parsePhoneNumberFromString(value) + if (!phoneNumber) return false + return phoneNumber.isValid() + }, + required: true, + }, +} + +const FormDeactivatedSmsCountSchema = new Schema( + bounceSmsCountSchema, +) + +const BouncedSubmissionSmsCountSchema = new Schema( + bounceSmsCountSchema, +) + const compileSmsCountModel = (db: Mongoose) => { const SmsCountSchema = new Schema( { @@ -69,10 +111,10 @@ const compileSmsCountModel = (db: Mongoose) => { SmsCountSchema.statics.logSms = async function ( this: ISmsCountModel, - { otpData, msgSrvcSid, smsType, logType }: LogSmsParams, + { smsData, msgSrvcSid, smsType, logType }: LogSmsParams, ) { const schemaData: Omit = { - ...otpData, + ...smsData, msgSrvcSid, smsType, logType, @@ -89,8 +131,16 @@ const compileSmsCountModel = (db: Mongoose) => { ) // Adding Discriminators - SmsCountModel.discriminator(SmsType.verification, VerificationSmsCountSchema) - SmsCountModel.discriminator(SmsType.adminContact, AdminContactSmsCountSchema) + SmsCountModel.discriminator(SmsType.Verification, VerificationSmsCountSchema) + SmsCountModel.discriminator(SmsType.AdminContact, AdminContactSmsCountSchema) + SmsCountModel.discriminator( + SmsType.DeactivatedForm, + FormDeactivatedSmsCountSchema, + ) + SmsCountModel.discriminator( + SmsType.BouncedSubmission, + BouncedSubmissionSmsCountSchema, + ) return SmsCountModel } diff --git a/src/app/utils/assert-unreachable.ts b/src/app/utils/assert-unreachable.ts deleted file mode 100644 index bb4fca757f..0000000000 --- a/src/app/utils/assert-unreachable.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Typescript type guard that asserts that all switch cases are exhaustive. - * Use to get compile-time safety for making sure all the cases are handled. - * - * See: - * https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript - */ -export const assertUnreachable = (switchCase: never): never => { - throw new Error(`This should never be reached in TypeScript: "${switchCase}"`) -} diff --git a/src/config/feature-manager/util/FeatureManager.class.ts b/src/config/feature-manager/util/FeatureManager.class.ts index 77e5f7f146..0e4ebaee36 100644 --- a/src/config/feature-manager/util/FeatureManager.class.ts +++ b/src/config/feature-manager/util/FeatureManager.class.ts @@ -14,6 +14,10 @@ const logger = createLoggerWithLabel(module) convict.addFormat(validator.url) export default class FeatureManager { + /* eslint-disable typesafe/no-throw-sync-func + -------- + This class bootstraps the application and should + cause a crash in case of errors. */ public states: Partial> // Map some feature names to some env vars private properties: Partial @@ -115,4 +119,5 @@ export default class FeatureManager { props: this.props(name), } } + /* eslint-enable typesafe/no-throw-sync-func */ } diff --git a/src/config/logger.ts b/src/config/logger.ts index 2254a39f3e..b3a793e822 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -197,6 +197,7 @@ const createCustomLogger = (logger: Logger) => { const { message, meta } = params // Not the expected shape, throw to catch block. if (!message || isEmpty(meta)) { + //eslint-disable-next-line throw new Error('Wrong shape') } return logger.info(message, { meta }) @@ -209,6 +210,7 @@ const createCustomLogger = (logger: Logger) => { const { message, meta, error } = params // Not the expected shape, throw to catch block. if (!message || isEmpty(meta)) { + //eslint-disable-next-line throw new Error('Wrong shape') } if (error) { @@ -224,6 +226,7 @@ const createCustomLogger = (logger: Logger) => { const { message, meta, error } = params // Not the expected shape, throw to catch block. if (!message || isEmpty(meta)) { + //eslint-disable-next-line throw new Error('Wrong shape') } if (error) { diff --git a/src/config/schema.ts b/src/config/schema.ts index c1c7cba7f2..34308b7dc8 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -34,7 +34,7 @@ convict.addFormat({ /** * Verifies that S3 bucket url is a valid url with or without trailing slash */ -const validateBucketUrl = ( +const validateS3BucketUrl = ( val: string, { isDev, @@ -42,6 +42,9 @@ const validateBucketUrl = ( region, }: { isDev: boolean; hasTrailingSlash: boolean; region: string }, ) => { + /* eslint-disable typesafe/no-throw-sync-func + -------- + The convict package expects format validation functions to throw Errors */ if (!validator.isURL(val, { require_tld: !isDev })) { throw new Error('must be a url') } @@ -59,6 +62,7 @@ const validateBucketUrl = ( if (!isDev && !isRegionCorrect.test(val)) { throw new Error(`region should be ${region}`) } + /* eslint-enable typesafe/no-throw-sync-func */ } // If the default value does not match the format specified, the configuration built from this schema @@ -352,7 +356,7 @@ export const loadS3BucketUrlSchema = ({ endPoint: { doc: 'Endpoint for S3 buckets', format: (val) => - validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + validateS3BucketUrl(val, { isDev, hasTrailingSlash: false, region }), default: 'https://s3.ap-southeast-1.amazonaws.com', // NOTE NO TRAILING / AT THE END OF THIS URL! env: 'AWS_ENDPOINT', }, @@ -360,19 +364,19 @@ export const loadS3BucketUrlSchema = ({ doc: 'Url of attachment S3 bucket derived from S3 endpoint and bucket name', format: (val) => - validateBucketUrl(val, { isDev, hasTrailingSlash: true, region }), + validateS3BucketUrl(val, { isDev, hasTrailingSlash: true, region }), default: null, }, logoBucketUrl: { doc: 'Url of logo S3 bucket derived from S3 endpoint and bucket name', format: (val) => - validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + validateS3BucketUrl(val, { isDev, hasTrailingSlash: false, region }), default: null, }, imageBucketUrl: { doc: 'Url of images S3 bucket derived from S3 endpoint and bucket name', format: (val) => - validateBucketUrl(val, { isDev, hasTrailingSlash: false, region }), + validateS3BucketUrl(val, { isDev, hasTrailingSlash: false, region }), default: null, }, } diff --git a/src/public/modules/users/controllers/authentication.client.controller.js b/src/public/modules/users/controllers/authentication.client.controller.js index c35a825202..403c9b2861 100755 --- a/src/public/modules/users/controllers/authentication.client.controller.js +++ b/src/public/modules/users/controllers/authentication.client.controller.js @@ -5,6 +5,7 @@ const HttpStatus = require('http-status-codes') angular .module('users') .controller('AuthenticationController', [ + '$scope', '$state', '$timeout', '$window', @@ -13,11 +14,21 @@ angular AuthenticationController, ]) -function AuthenticationController($state, $timeout, $window, Auth, GTag) { +function AuthenticationController( + $scope, + $state, + $timeout, + $window, + Auth, + GTag, +) { const vm = this + let notifDelayTimeout + vm.credentials = {} vm.buttonClicked = false + vm.showOtpDelayNotification = false // logic/booleans to show sign in process sequentially. 2 possible values. // 'email' - email input @@ -161,6 +172,7 @@ function AuthenticationController($state, $timeout, $window, Auth, GTag) { vm.signInMsg.isMsg = false vm.isOtpSending = true vm.buttonClicked = true + vm.showOtpDelayNotification = false const { email } = vm.credentials Auth.sendOtp({ email }).then( function (success) { @@ -176,6 +188,13 @@ function AuthenticationController($state, $timeout, $window, Auth, GTag) { angular.element('#otp-input').focus() angular.element('#otp-input').select() }, 100) + + // Cancel existing timeout and set new one. + cancelNotifDelayTimeout() + notifDelayTimeout = $timeout(function () { + vm.signInMsg.isMsg = false + vm.showOtpDelayNotification = true + }, 20000) }, function (error) { vm.isOtpSending = false @@ -189,10 +208,6 @@ function AuthenticationController($state, $timeout, $window, Auth, GTag) { isError: true, msg, } - $timeout(function () { - angular.element('#otp-input').focus() - angular.element('#otp-input').select() - }, 100) }, ) } @@ -245,4 +260,14 @@ function AuthenticationController($state, $timeout, $window, Auth, GTag) { }, ) } + + const cancelNotifDelayTimeout = () => { + if (notifDelayTimeout) { + $timeout.cancel(notifDelayTimeout) + } + } + + $scope.$on('$destroy', function () { + cancelNotifDelayTimeout() + }) } diff --git a/src/public/modules/users/views/authentication/signin.client.view.html b/src/public/modules/users/views/authentication/signin.client.view.html index b7c7342a73..48d6f7fb90 100755 --- a/src/public/modules/users/views/authentication/signin.client.view.html +++ b/src/public/modules/users/views/authentication/signin.client.view.html @@ -92,10 +92,23 @@ > {{vm.signInMsg.msg}} -
+
Sending OTP...
+
+ + + OTP might be delayed due to government email traffic. If you are + unable to sign in, please try again later, or contact us. + +