diff --git a/backend/email-templates/reset-password.html b/backend/email-templates/reset-password.html new file mode 100644 index 000000000..ff6313a8e --- /dev/null +++ b/backend/email-templates/reset-password.html @@ -0,0 +1,137 @@ + + + + @import + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"; + .btn table{ width: 100%; } .btn a{ width: 100%; padding: 10px 0 + !important;} + + + + + + + + + + + + + + + Hey, {{name}} + + + Nobody likes being locked out of their account. We're coming to your + rescue - just click the button below to get started. If you didn't + request a password reset, you can safely ignore this email. + + + + Reset your password + + + + Cheers, + + + Monkeytype Team + + + + + + Alternatively, you can copy and paste the link below into your + browser: + + + {{passwordResetLink}} + + + + + + + + + + + + + + + + + + + + + + + + + + monkeytype.com + + + + + + diff --git a/backend/email-templates/verification.html b/backend/email-templates/verification.html new file mode 100644 index 000000000..930629ad5 --- /dev/null +++ b/backend/email-templates/verification.html @@ -0,0 +1,137 @@ + + + + @import + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"; + .btn table{ width: 100%; } .btn a{ width: 100%; padding: 10px 0 + !important;} + + + + + + + + + + + + + + + Hey, {{name}} + + + Thanks for joining Monkeytype! We just need one more thing from you + - a quick confirmation of your email address and you'll be all set. + Click the button below to get started: + + + + Verify + + + + Cheers, + + + Monkeytype Team + + + + + + Alternatively, you can copy and paste the link below into your + browser: + + + {{verificationLink}} + + + + + + + + + + + + + + + + + + + + + + + + + + monkeytype.com + + + + + + diff --git a/backend/example.env b/backend/example.env index 57f0b95b6..bf36a3ea2 100644 --- a/backend/example.env +++ b/backend/example.env @@ -13,3 +13,11 @@ MODE=dev # DB_PASSWORD= # DB_AUTH_MECHANISM="SCRAM-SHA-256" # DB_AUTH_SOURCE=admin + +# You can get a testing email address over at +# https://ethereal.email/create +# +# EMAIL_PORT=587 +# EMAIL_HOST=smtp.ethereal.email +# EMAIL_USER= +# EMAIL_PASS= diff --git a/backend/package-lock.json b/backend/package-lock.json index b2eec43b6..196042e6c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,14 +23,17 @@ "joi": "17.6.0", "lodash": "4.17.21", "lru-cache": "7.10.1", + "mjml": "4.13.0", "mongodb": "4.4.0", + "mustache": "4.2.0", "node-fetch": "2.6.7", + "nodemailer": "6.9.1", "nodemon": "2.0.17", "object-hash": "3.0.0", "path": "0.12.7", "prom-client": "14.0.1", "rate-limiter-flexible": "2.3.7", - "simple-git": "^3.16.0", + "simple-git": "3.16.0", "string-similarity": "4.0.4", "swagger-stats": "0.99.4", "swagger-ui-express": "4.3.0", @@ -46,8 +49,10 @@ "@types/ioredis": "4.28.10", "@types/jest": "27.5.0", "@types/lodash": "4.14.178", + "@types/mustache": "4.2.2", "@types/node": "17.0.18", "@types/node-fetch": "2.6.1", + "@types/nodemailer": "6.4.7", "@types/object-hash": "2.2.1", "@types/supertest": "2.0.12", "@types/swagger-stats": "0.95.4", @@ -533,6 +538,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.16.7", "dev": true, @@ -1741,6 +1757,12 @@ "@types/node": "*" } }, + "node_modules/@types/mustache": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.2.tgz", + "integrity": "sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==", + "dev": true + }, "node_modules/@types/node": { "version": "17.0.18", "license": "MIT" @@ -1767,6 +1789,15 @@ "node": ">= 6" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz", + "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/object-hash": { "version": "2.2.1", "dev": true, @@ -2023,6 +2054,14 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -2418,6 +2457,11 @@ "node": ">= 0.8" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boxen": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", @@ -2731,6 +2775,15 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, "node_modules/camelcase": { "version": "5.3.1", "dev": true, @@ -2809,6 +2862,41 @@ "node": ">=10" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.5.3", "funding": [ @@ -2854,6 +2942,17 @@ "dev": true, "license": "MIT" }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -2867,7 +2966,6 @@ }, "node_modules/cliui": { "version": "7.0.4", - "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2962,6 +3060,11 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/commondir": { "version": "1.0.1", "dev": true, @@ -3181,6 +3284,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.4.4", "dev": true, @@ -3375,6 +3504,11 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, "node_modules/dezalgo": { "version": "1.0.3", "dev": true, @@ -3400,6 +3534,30 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "2.0.1", "dev": true, @@ -3419,6 +3577,33 @@ "node": ">=8" } }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -3480,6 +3665,34 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dependencies": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/editorconfig/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/editorconfig/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" @@ -3533,7 +3746,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "optional": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -3553,7 +3765,6 @@ }, "node_modules/escalade": { "version": "3.1.1", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -4246,7 +4457,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -4528,6 +4738,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, "node_modules/helmet": { "version": "4.6.0", "license": "MIT", @@ -4559,6 +4777,44 @@ "dev": true, "license": "MIT" }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -5778,6 +6034,76 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-beautify": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.7.tgz", + "integrity": "sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^8.0.3", + "nopt": "^6.0.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -6124,6 +6450,32 @@ "node": ">=0.6.0" } }, + "node_modules/juice": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-7.0.0.tgz", + "integrity": "sha512-AjKQX31KKN+uJs+zaf+GW8mBO/f/0NqSh2moTMyvwBY+4/lXIYTU8D8I2h6BAV3Xnz6GGsbalUyFqbYMe+Vh+Q==", + "dependencies": { + "cheerio": "^1.0.0-rc.3", + "commander": "^5.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^5.0.0" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -6369,6 +6721,11 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -6525,6 +6882,11 @@ "license": "MIT", "optional": true }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "license": "MIT" @@ -6634,6 +6996,404 @@ "node": ">= 8" } }, + "node_modules/mjml": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.13.0.tgz", + "integrity": "sha512-OnFKESouLshz8DPFSb6M/dE8GkhiJnoy6LAam5TiLA1anAj24yQ2ZH388LtQoEkvTisqwiTmc9ejDh5ctnFaJQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "mjml-cli": "4.13.0", + "mjml-core": "4.13.0", + "mjml-migrate": "4.13.0", + "mjml-preset-core": "4.13.0", + "mjml-validator": "4.13.0" + }, + "bin": { + "mjml": "bin/mjml" + } + }, + "node_modules/mjml-accordion": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.13.0.tgz", + "integrity": "sha512-E3yihZW5Oq2p+sWOcr8kWeRTROmiTYOGxB4IOxW/jTycdY07N3FX3e6vuh7Fv3rryHEUaydUQYto3ICVyctI7w==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-body": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.13.0.tgz", + "integrity": "sha512-S4HgwAuO9dEsyX9sr6WBf9/xr+H2ASVaLn22aurJm1S2Lvc1wifLPYBQgFmNdCjaesTCNtOMUDpG+Rbnavyaqg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-button": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.13.0.tgz", + "integrity": "sha512-3y8IAHCCxh7ESHh1aOOqobZKUgyNxOKAGQ9TlJoyaLpsKUFzkN8nmrD0KXF0ADSuzvhMZ1CdRIJuZ5mjv2TwWQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-carousel": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.13.0.tgz", + "integrity": "sha512-ORSY5bEYlMlrWSIKI/lN0Tz3uGltWAjG8DQl2Yr3pwjwOaIzGE+kozrDf+T9xItfiIIbvKajef1dg7B7XgP0zg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-cli": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.13.0.tgz", + "integrity": "sha512-kAZxpH0QqlTF/CcLzELgKw1ljKRxrmWJ310CJQhbPAxHvwQ/nIb+q82U+zRJAelRPPKjnOb+hSrMRqTgk9rH3w==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "chokidar": "^3.0.0", + "glob": "^7.1.1", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-migrate": "4.13.0", + "mjml-parser-xml": "4.13.0", + "mjml-validator": "4.13.0", + "yargs": "^16.1.0" + }, + "bin": { + "mjml-cli": "bin/mjml" + } + }, + "node_modules/mjml-column": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.13.0.tgz", + "integrity": "sha512-O8FrWKK/bCy9XpKxrKRYWNdgWNaVd4TK4RqMeVI/I70IbnYnc1uf15jnsPMxCBSbT+NyXyk8k7fn099797uwpw==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.13.0.tgz", + "integrity": "sha512-kU5AoVTlZaXR/EDi3ix66xpzUe+kScYus71lBH/wo/B+LZW70GHE1AYWtsog5oJp1MuTHpMFTNuBD/wePeEgWg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "cheerio": "1.0.0-rc.10", + "detect-node": "2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^7.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.13.0", + "mjml-parser-xml": "4.13.0", + "mjml-validator": "4.13.0" + } + }, + "node_modules/mjml-divider": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.13.0.tgz", + "integrity": "sha512-ooPCwfmxEC+wJduqObYezMp7W5UCHjL9Y1LPB5FGna2FrOejgfd6Ix3ij8Wrmycmlol7E2N4D7c5NDH5DbRCJg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-group": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.13.0.tgz", + "integrity": "sha512-U7E8m8aaoAE/dMqjqXPjjrKcwO36B4cquAy9ASldECrIZJBcpFYO6eYf5yLXrNCUM2P0id8pgVjrUq23s00L7Q==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.13.0.tgz", + "integrity": "sha512-sL2qQuoVALXBCiemu4DPo9geDr8DuUdXVJxm+4nd6k5jpLCfSDmFlNhgSsLPzsYn7VEac3/sxsjLtomQ+6/BHg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-attributes": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.13.0.tgz", + "integrity": "sha512-haggCafno+0lQylxJStkINCVCPMwfTpwE6yjCHeGOpQl/TkoNmjNkDr7DEEbNTZbt4Ekg070lQFn7clDy38EoA==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-breakpoint": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.13.0.tgz", + "integrity": "sha512-D2iPDeUKQK1+rYSNa2HGOvgfPxZhNyndTG0iBEb/FxdGge2hbeDCZEN0mwDYE3wWB+qSBqlCuMI+Vr4pEjZbKg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-font": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.13.0.tgz", + "integrity": "sha512-mYn8aWnbrEap5vX2b4662hkUv6WifcYzYn++Yi6OHrJQi55LpzcU+myAGpfQEXXrpU8vGwExMTFKsJq5n2Kaow==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-html-attributes": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.13.0.tgz", + "integrity": "sha512-m30Oro297+18Zou/1qYjagtmCOWtYXeoS38OABQ5zOSzMItE3TcZI9JNcOueIIWIyFCETe8StrTAKcQ2GHwsDw==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-preview": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.13.0.tgz", + "integrity": "sha512-v0K/NocjFCbaoF/0IMVNmiqov91HxqT07vNTEl0Bt9lKFrTKVC01m1S4K7AB78T/bEeJ/HwmNjr1+TMtVNGGow==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-style": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.13.0.tgz", + "integrity": "sha512-tBa33GL9Atn5bAM2UwE+uxv4rI29WgX/e5lXX+5GWlsb4thmiN6rxpFTNqBqWbBNRbZk4UEZF78M7Da8xC1ZGQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-head-title": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.13.0.tgz", + "integrity": "sha512-Mq0bjuZXJlwxfVcjuYihQcigZSDTKeQaG3nORR1D0jsOH2BXU4XgUK1UOcTXn2qCBIfRoIMq7rfzYs+L0CRhdw==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-hero": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.13.0.tgz", + "integrity": "sha512-aWEOScdrhyjwdKBWG4XQaElRHP8LU5PtktkpMeBXa4yxrxNs25qRnDqMNkjSrnnmFKWZmQ166tfboY6RBNf0UA==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-image": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.13.0.tgz", + "integrity": "sha512-agMmm2wRZTIrKwrUnYFlnAbtrKYSP0R2en+Vf92HPspAwmaw3/AeOW/QxmSiMhfGf+xsEJyzVvR/nd33jbT3sg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-migrate": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.13.0.tgz", + "integrity": "sha512-I1euHiAyNpaz+B5vH+Z4T+hg/YtI5p3PqQ3/zTLv8gi24V6BILjTaftWhH5+3R/gQkQhH0NUaWNnRmds+Mq5DQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-parser-xml": "4.13.0", + "yargs": "^16.1.0" + }, + "bin": { + "migrate": "lib/cli.js" + } + }, + "node_modules/mjml-navbar": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.13.0.tgz", + "integrity": "sha512-0Oqyyk+OdtXfsjswRb/7Ql1UOjN4MbqFPKoyltJqtj+11MRpF5+Wjd74Dj9H7l81GFwkIB9OaP+ZMiD+TPECgg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-parser-xml": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.13.0.tgz", + "integrity": "sha512-phljtI8DaW++q0aybR/Ykv9zCyP/jCFypxVNo26r2IQo//VYXyc7JuLZZT8N/LAI8lZcwbTVxQPBzJTmZ5IfwQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "detect-node": "2.0.4", + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15" + } + }, + "node_modules/mjml-parser-xml/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/mjml-parser-xml/node_modules/htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, + "node_modules/mjml-preset-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.13.0.tgz", + "integrity": "sha512-gxzYaKkvUrHuzT1oqjEPSDtdmgEnN99Hf5f1r2CR5aMOB1x66EA3T8ATvF1o7qrBTVV4KMVlQem3IubMSYJZRw==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "mjml-accordion": "4.13.0", + "mjml-body": "4.13.0", + "mjml-button": "4.13.0", + "mjml-carousel": "4.13.0", + "mjml-column": "4.13.0", + "mjml-divider": "4.13.0", + "mjml-group": "4.13.0", + "mjml-head": "4.13.0", + "mjml-head-attributes": "4.13.0", + "mjml-head-breakpoint": "4.13.0", + "mjml-head-font": "4.13.0", + "mjml-head-html-attributes": "4.13.0", + "mjml-head-preview": "4.13.0", + "mjml-head-style": "4.13.0", + "mjml-head-title": "4.13.0", + "mjml-hero": "4.13.0", + "mjml-image": "4.13.0", + "mjml-navbar": "4.13.0", + "mjml-raw": "4.13.0", + "mjml-section": "4.13.0", + "mjml-social": "4.13.0", + "mjml-spacer": "4.13.0", + "mjml-table": "4.13.0", + "mjml-text": "4.13.0", + "mjml-wrapper": "4.13.0" + } + }, + "node_modules/mjml-raw": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.13.0.tgz", + "integrity": "sha512-JbBYxwX1a/zbqnCrlDCRNqov2xqUrMCaEdTHfqE2athj479aQXvLKFM20LilTMaClp/dR0yfvFLfFVrC5ej4FQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-section": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.13.0.tgz", + "integrity": "sha512-BLcqlhavtRakKtzDQPLv6Ae4Jt4imYWq/P0jo+Sjk7tP4QifgVA2KEQOirPK5ZUqw/lvK7Afhcths5rXZ2ItnQ==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-social": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.13.0.tgz", + "integrity": "sha512-zL2a7Wwsk8OXF0Bqu+1B3La1UPwdTMcEXptO8zdh2V5LL6Xb7Gfyvx6w0CmmBtG5IjyCtqaKy5wtrcpG9Hvjfg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-spacer": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.13.0.tgz", + "integrity": "sha512-Acw4QJ0MJ38W4IewXuMX7hLaW1BZaln+gEEuTfrv0xwPdTxX1ILqz4r+s9mYMxYkIDLWMCjBvXyQK6aWlid13A==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-table": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.13.0.tgz", + "integrity": "sha512-UAWPVMaGReQhf776DFdiwdcJTIHTek3zzQ1pb+E7VlypEYgIpFvdUJ39UIiiflhqtdBATmHwKBOtePwU0MzFMg==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-text": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.13.0.tgz", + "integrity": "sha512-uDuraaQFdu+6xfuigCimbeznnOnJfwRdcCL1lTBTusTuEvW/5Va6m2D3mnMeEpl+bp4+cxesXIz9st6A9pcg5A==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "node_modules/mjml-validator": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.13.0.tgz", + "integrity": "sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==", + "dependencies": { + "@babel/runtime": "^7.14.6" + } + }, + "node_modules/mjml-wrapper": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.13.0.tgz", + "integrity": "sha512-p/44JvHg04rAFR7QDImg8nZucEokIjFH6KJMHxsO0frJtLZ+IuakctzlZAADHsqiR52BwocDsXSa+o9SE2l6Ng==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-section": "4.13.0" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "license": "MIT", @@ -6909,6 +7669,14 @@ "node-gyp-build": "^4.2.3" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nan": { "version": "2.15.0", "license": "MIT", @@ -6959,6 +7727,14 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dependencies": { + "lower-case": "^1.1.1" + } + }, "node_modules/node-addon-api": { "version": "3.2.1", "license": "MIT" @@ -7009,6 +7785,14 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", + "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.17.tgz", @@ -7097,6 +7881,17 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.0", "dev": true, @@ -7309,6 +8104,14 @@ "node": ">=10" } }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "dependencies": { + "no-case": "^2.2.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "dev": true, @@ -7328,9 +8131,16 @@ }, "node_modules/parse5": { "version": "6.0.1", - "dev": true, "license": "MIT" }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -7879,6 +8689,11 @@ "node": ">=4" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/registry-auth-token": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", @@ -7904,6 +8719,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/request": { "version": "2.88.2", "license": "Apache-2.0", @@ -7957,7 +8780,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8310,6 +9132,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==" + }, "node_modules/signal-exit": { "version": "3.0.7", "license": "ISC" @@ -8369,6 +9196,14 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", @@ -8400,7 +9235,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9410,7 +10244,6 @@ "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -9520,6 +10353,11 @@ "node": ">=10" } }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==" + }, "node_modules/uri-js": { "version": "4.4.1", "license": "BSD-2-Clause", @@ -9573,6 +10411,14 @@ "node": ">= 8" } }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "engines": { + "node": ">=10" + } + }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -9619,6 +10465,69 @@ "makeerror": "1.0.12" } }, + "node_modules/web-resource-inliner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz", + "integrity": "sha512-AIihwH+ZmdHfkJm7BjSXiEClVt4zUFqX4YlFAzjL13wLtDuUneSaFvDBTbdYRecs35SiU7iNKbMnN+++wVfb6A==", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^4.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "license": "BSD-2-Clause" @@ -9787,7 +10696,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9872,7 +10780,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -9884,7 +10791,6 @@ }, "node_modules/yargs": { "version": "16.2.0", - "devOptional": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -9901,7 +10807,6 @@ }, "node_modules/yargs-parser": { "version": "20.2.9", - "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -10235,6 +11140,14 @@ "@babel/helper-plugin-utils": "^7.16.7" } }, + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@babel/template": { "version": "7.16.7", "dev": true, @@ -11190,6 +12103,12 @@ "@types/node": "*" } }, + "@types/mustache": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.2.tgz", + "integrity": "sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==", + "dev": true + }, "@types/node": { "version": "17.0.18" }, @@ -11212,6 +12131,15 @@ } } }, + "@types/nodemailer": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz", + "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/object-hash": { "version": "2.2.1", "dev": true @@ -11404,6 +12332,11 @@ "string-width": "^4.1.0" } }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, "ansi-escapes": { "version": "4.3.2", "dev": true, @@ -11671,6 +12604,11 @@ "type-is": "~1.6.18" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "boxen": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", @@ -11864,6 +12802,15 @@ "version": "3.1.0", "dev": true }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, "camelcase": { "version": "5.3.1", "dev": true @@ -11906,6 +12853,32 @@ "version": "1.0.2", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "requires": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + } + }, + "cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "requires": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + } + }, "chokidar": { "version": "3.5.3", "requires": { @@ -11931,6 +12904,14 @@ "version": "1.2.2", "dev": true }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "requires": { + "source-map": "~0.6.0" + } + }, "cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -11938,7 +12919,6 @@ }, "cliui": { "version": "7.0.4", - "devOptional": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -12006,6 +12986,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "commondir": { "version": "1.0.1", "dev": true @@ -12160,6 +13145,23 @@ } } }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "cssom": { "version": "0.4.4", "dev": true @@ -12283,6 +13285,11 @@ "version": "3.1.0", "dev": true }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" + }, "dezalgo": { "version": "1.0.3", "dev": true, @@ -12299,6 +13306,21 @@ "version": "27.5.1", "dev": true }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "2.0.1", "dev": true, @@ -12312,6 +13334,24 @@ } } }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, "dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -12360,6 +13400,33 @@ "safe-buffer": "^5.0.1" } }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, "ee-first": { "version": "1.1.1" }, @@ -12396,8 +13463,7 @@ "entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "optional": true + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" }, "error-ex": { "version": "1.3.2", @@ -12413,8 +13479,7 @@ } }, "escalade": { - "version": "3.1.1", - "devOptional": true + "version": "3.1.1" }, "escape-goat": { "version": "4.0.0", @@ -12889,8 +13954,7 @@ "dev": true }, "get-caller-file": { - "version": "2.0.5", - "devOptional": true + "version": "2.0.5" }, "get-intrinsic": { "version": "1.1.1", @@ -13068,6 +14132,11 @@ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==" }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "helmet": { "version": "4.6.0" }, @@ -13086,6 +14155,31 @@ "version": "2.0.2", "dev": true }, + "html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "requires": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -13908,6 +15002,55 @@ "@panva/asn1.js": "^1.0.0" } }, + "js-beautify": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.7.tgz", + "integrity": "sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==", + "requires": { + "config-chain": "^1.1.13", + "editorconfig": "^0.15.3", + "glob": "^8.0.3", + "nopt": "^6.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "requires": { + "abbrev": "^1.0.0" + } + } + } + }, "js-tokens": { "version": "4.0.0", "dev": true @@ -14157,6 +15300,25 @@ "verror": "1.10.0" } }, + "juice": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-7.0.0.tgz", + "integrity": "sha512-AjKQX31KKN+uJs+zaf+GW8mBO/f/0NqSh2moTMyvwBY+4/lXIYTU8D8I2h6BAV3Xnz6GGsbalUyFqbYMe+Vh+Q==", + "requires": { + "cheerio": "^1.0.0-rc.3", + "commander": "^5.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^5.0.0" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + } + } + }, "jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -14355,6 +15517,11 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==" + }, "lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -14468,6 +15635,11 @@ "version": "1.5.0", "optional": true }, + "mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "merge-descriptors": { "version": "1.0.1" }, @@ -14529,6 +15701,391 @@ "yallist": "^4.0.0" } }, + "mjml": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.13.0.tgz", + "integrity": "sha512-OnFKESouLshz8DPFSb6M/dE8GkhiJnoy6LAam5TiLA1anAj24yQ2ZH388LtQoEkvTisqwiTmc9ejDh5ctnFaJQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "mjml-cli": "4.13.0", + "mjml-core": "4.13.0", + "mjml-migrate": "4.13.0", + "mjml-preset-core": "4.13.0", + "mjml-validator": "4.13.0" + } + }, + "mjml-accordion": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.13.0.tgz", + "integrity": "sha512-E3yihZW5Oq2p+sWOcr8kWeRTROmiTYOGxB4IOxW/jTycdY07N3FX3e6vuh7Fv3rryHEUaydUQYto3ICVyctI7w==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-body": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.13.0.tgz", + "integrity": "sha512-S4HgwAuO9dEsyX9sr6WBf9/xr+H2ASVaLn22aurJm1S2Lvc1wifLPYBQgFmNdCjaesTCNtOMUDpG+Rbnavyaqg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-button": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.13.0.tgz", + "integrity": "sha512-3y8IAHCCxh7ESHh1aOOqobZKUgyNxOKAGQ9TlJoyaLpsKUFzkN8nmrD0KXF0ADSuzvhMZ1CdRIJuZ5mjv2TwWQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-carousel": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.13.0.tgz", + "integrity": "sha512-ORSY5bEYlMlrWSIKI/lN0Tz3uGltWAjG8DQl2Yr3pwjwOaIzGE+kozrDf+T9xItfiIIbvKajef1dg7B7XgP0zg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-cli": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.13.0.tgz", + "integrity": "sha512-kAZxpH0QqlTF/CcLzELgKw1ljKRxrmWJ310CJQhbPAxHvwQ/nIb+q82U+zRJAelRPPKjnOb+hSrMRqTgk9rH3w==", + "requires": { + "@babel/runtime": "^7.14.6", + "chokidar": "^3.0.0", + "glob": "^7.1.1", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-migrate": "4.13.0", + "mjml-parser-xml": "4.13.0", + "mjml-validator": "4.13.0", + "yargs": "^16.1.0" + } + }, + "mjml-column": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.13.0.tgz", + "integrity": "sha512-O8FrWKK/bCy9XpKxrKRYWNdgWNaVd4TK4RqMeVI/I70IbnYnc1uf15jnsPMxCBSbT+NyXyk8k7fn099797uwpw==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.13.0.tgz", + "integrity": "sha512-kU5AoVTlZaXR/EDi3ix66xpzUe+kScYus71lBH/wo/B+LZW70GHE1AYWtsog5oJp1MuTHpMFTNuBD/wePeEgWg==", + "requires": { + "@babel/runtime": "^7.14.6", + "cheerio": "1.0.0-rc.10", + "detect-node": "2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^7.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.13.0", + "mjml-parser-xml": "4.13.0", + "mjml-validator": "4.13.0" + } + }, + "mjml-divider": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.13.0.tgz", + "integrity": "sha512-ooPCwfmxEC+wJduqObYezMp7W5UCHjL9Y1LPB5FGna2FrOejgfd6Ix3ij8Wrmycmlol7E2N4D7c5NDH5DbRCJg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-group": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.13.0.tgz", + "integrity": "sha512-U7E8m8aaoAE/dMqjqXPjjrKcwO36B4cquAy9ASldECrIZJBcpFYO6eYf5yLXrNCUM2P0id8pgVjrUq23s00L7Q==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.13.0.tgz", + "integrity": "sha512-sL2qQuoVALXBCiemu4DPo9geDr8DuUdXVJxm+4nd6k5jpLCfSDmFlNhgSsLPzsYn7VEac3/sxsjLtomQ+6/BHg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-attributes": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.13.0.tgz", + "integrity": "sha512-haggCafno+0lQylxJStkINCVCPMwfTpwE6yjCHeGOpQl/TkoNmjNkDr7DEEbNTZbt4Ekg070lQFn7clDy38EoA==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-breakpoint": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.13.0.tgz", + "integrity": "sha512-D2iPDeUKQK1+rYSNa2HGOvgfPxZhNyndTG0iBEb/FxdGge2hbeDCZEN0mwDYE3wWB+qSBqlCuMI+Vr4pEjZbKg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-font": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.13.0.tgz", + "integrity": "sha512-mYn8aWnbrEap5vX2b4662hkUv6WifcYzYn++Yi6OHrJQi55LpzcU+myAGpfQEXXrpU8vGwExMTFKsJq5n2Kaow==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-html-attributes": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.13.0.tgz", + "integrity": "sha512-m30Oro297+18Zou/1qYjagtmCOWtYXeoS38OABQ5zOSzMItE3TcZI9JNcOueIIWIyFCETe8StrTAKcQ2GHwsDw==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-preview": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.13.0.tgz", + "integrity": "sha512-v0K/NocjFCbaoF/0IMVNmiqov91HxqT07vNTEl0Bt9lKFrTKVC01m1S4K7AB78T/bEeJ/HwmNjr1+TMtVNGGow==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-style": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.13.0.tgz", + "integrity": "sha512-tBa33GL9Atn5bAM2UwE+uxv4rI29WgX/e5lXX+5GWlsb4thmiN6rxpFTNqBqWbBNRbZk4UEZF78M7Da8xC1ZGQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-head-title": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.13.0.tgz", + "integrity": "sha512-Mq0bjuZXJlwxfVcjuYihQcigZSDTKeQaG3nORR1D0jsOH2BXU4XgUK1UOcTXn2qCBIfRoIMq7rfzYs+L0CRhdw==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-hero": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.13.0.tgz", + "integrity": "sha512-aWEOScdrhyjwdKBWG4XQaElRHP8LU5PtktkpMeBXa4yxrxNs25qRnDqMNkjSrnnmFKWZmQ166tfboY6RBNf0UA==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-image": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.13.0.tgz", + "integrity": "sha512-agMmm2wRZTIrKwrUnYFlnAbtrKYSP0R2en+Vf92HPspAwmaw3/AeOW/QxmSiMhfGf+xsEJyzVvR/nd33jbT3sg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-migrate": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.13.0.tgz", + "integrity": "sha512-I1euHiAyNpaz+B5vH+Z4T+hg/YtI5p3PqQ3/zTLv8gi24V6BILjTaftWhH5+3R/gQkQhH0NUaWNnRmds+Mq5DQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-parser-xml": "4.13.0", + "yargs": "^16.1.0" + } + }, + "mjml-navbar": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.13.0.tgz", + "integrity": "sha512-0Oqyyk+OdtXfsjswRb/7Ql1UOjN4MbqFPKoyltJqtj+11MRpF5+Wjd74Dj9H7l81GFwkIB9OaP+ZMiD+TPECgg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-parser-xml": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.13.0.tgz", + "integrity": "sha512-phljtI8DaW++q0aybR/Ykv9zCyP/jCFypxVNo26r2IQo//VYXyc7JuLZZT8N/LAI8lZcwbTVxQPBzJTmZ5IfwQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "detect-node": "2.0.4", + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15" + }, + "dependencies": { + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + } + } + }, + "mjml-preset-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.13.0.tgz", + "integrity": "sha512-gxzYaKkvUrHuzT1oqjEPSDtdmgEnN99Hf5f1r2CR5aMOB1x66EA3T8ATvF1o7qrBTVV4KMVlQem3IubMSYJZRw==", + "requires": { + "@babel/runtime": "^7.14.6", + "mjml-accordion": "4.13.0", + "mjml-body": "4.13.0", + "mjml-button": "4.13.0", + "mjml-carousel": "4.13.0", + "mjml-column": "4.13.0", + "mjml-divider": "4.13.0", + "mjml-group": "4.13.0", + "mjml-head": "4.13.0", + "mjml-head-attributes": "4.13.0", + "mjml-head-breakpoint": "4.13.0", + "mjml-head-font": "4.13.0", + "mjml-head-html-attributes": "4.13.0", + "mjml-head-preview": "4.13.0", + "mjml-head-style": "4.13.0", + "mjml-head-title": "4.13.0", + "mjml-hero": "4.13.0", + "mjml-image": "4.13.0", + "mjml-navbar": "4.13.0", + "mjml-raw": "4.13.0", + "mjml-section": "4.13.0", + "mjml-social": "4.13.0", + "mjml-spacer": "4.13.0", + "mjml-table": "4.13.0", + "mjml-text": "4.13.0", + "mjml-wrapper": "4.13.0" + } + }, + "mjml-raw": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.13.0.tgz", + "integrity": "sha512-JbBYxwX1a/zbqnCrlDCRNqov2xqUrMCaEdTHfqE2athj479aQXvLKFM20LilTMaClp/dR0yfvFLfFVrC5ej4FQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-section": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.13.0.tgz", + "integrity": "sha512-BLcqlhavtRakKtzDQPLv6Ae4Jt4imYWq/P0jo+Sjk7tP4QifgVA2KEQOirPK5ZUqw/lvK7Afhcths5rXZ2ItnQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-social": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.13.0.tgz", + "integrity": "sha512-zL2a7Wwsk8OXF0Bqu+1B3La1UPwdTMcEXptO8zdh2V5LL6Xb7Gfyvx6w0CmmBtG5IjyCtqaKy5wtrcpG9Hvjfg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-spacer": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.13.0.tgz", + "integrity": "sha512-Acw4QJ0MJ38W4IewXuMX7hLaW1BZaln+gEEuTfrv0xwPdTxX1ILqz4r+s9mYMxYkIDLWMCjBvXyQK6aWlid13A==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-table": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.13.0.tgz", + "integrity": "sha512-UAWPVMaGReQhf776DFdiwdcJTIHTek3zzQ1pb+E7VlypEYgIpFvdUJ39UIiiflhqtdBATmHwKBOtePwU0MzFMg==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-text": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.13.0.tgz", + "integrity": "sha512-uDuraaQFdu+6xfuigCimbeznnOnJfwRdcCL1lTBTusTuEvW/5Va6m2D3mnMeEpl+bp4+cxesXIz9st6A9pcg5A==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0" + } + }, + "mjml-validator": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.13.0.tgz", + "integrity": "sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==", + "requires": { + "@babel/runtime": "^7.14.6" + } + }, + "mjml-wrapper": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.13.0.tgz", + "integrity": "sha512-p/44JvHg04rAFR7QDImg8nZucEokIjFH6KJMHxsO0frJtLZ+IuakctzlZAADHsqiR52BwocDsXSa+o9SE2l6Ng==", + "requires": { + "@babel/runtime": "^7.14.6", + "lodash": "^4.17.21", + "mjml-core": "4.13.0", + "mjml-section": "4.13.0" + } + }, "mkdirp": { "version": "1.0.4" }, @@ -14702,6 +16259,11 @@ "node-gyp-build": "^4.2.3" } }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==" + }, "nan": { "version": "2.15.0", "optional": true @@ -14734,6 +16296,14 @@ } } }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "requires": { + "lower-case": "^1.1.1" + } + }, "node-addon-api": { "version": "3.2.1" }, @@ -14760,6 +16330,11 @@ "version": "2.0.4", "dev": true }, + "nodemailer": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", + "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==" + }, "nodemon": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.17.tgz", @@ -14818,6 +16393,14 @@ "set-blocking": "^2.0.0" } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.0", "dev": true @@ -14950,6 +16533,14 @@ } } }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "requires": { + "no-case": "^2.2.0" + } + }, "parse-json": { "version": "5.2.0", "dev": true, @@ -14961,8 +16552,15 @@ } }, "parse5": { + "version": "6.0.1" + }, + "parse5-htmlparser2-tree-adapter": { "version": "6.0.1", - "dev": true + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "requires": { + "parse5": "^6.0.1" + } }, "parseurl": { "version": "1.3.3" @@ -15330,6 +16928,11 @@ "redis-errors": "^1.0.0" } }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "registry-auth-token": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", @@ -15346,6 +16949,11 @@ "rc": "1.2.8" } }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==" + }, "request": { "version": "2.88.2", "requires": { @@ -15384,8 +16992,7 @@ "dev": true }, "require-directory": { - "version": "2.1.1", - "devOptional": true + "version": "2.1.1" }, "require-from-string": { "version": "2.0.2", @@ -15624,6 +17231,11 @@ "object-inspect": "^1.9.0" } }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==" + }, "signal-exit": { "version": "3.0.7" }, @@ -15666,6 +17278,11 @@ "version": "3.0.0", "dev": true }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==" + }, "smart-buffer": { "version": "4.2.0" }, @@ -15685,8 +17302,7 @@ } }, "source-map": { - "version": "0.6.1", - "devOptional": true + "version": "0.6.1" }, "source-map-support": { "version": "0.5.21", @@ -16339,8 +17955,7 @@ "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" }, "undefsafe": { "version": "2.0.5" @@ -16410,6 +18025,11 @@ } } }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==" + }, "uri-js": { "version": "4.4.1", "requires": { @@ -16446,6 +18066,11 @@ } } }, + "valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==" + }, "vary": { "version": "1.1.2" }, @@ -16478,6 +18103,50 @@ "makeerror": "1.0.12" } }, + "web-resource-inliner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz", + "integrity": "sha512-AIihwH+ZmdHfkJm7BjSXiEClVt4zUFqX4YlFAzjL13wLtDuUneSaFvDBTbdYRecs35SiU7iNKbMnN+++wVfb6A==", + "requires": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^4.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "dependencies": { + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==" + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + } + } + }, "webidl-conversions": { "version": "3.0.1" }, @@ -16594,7 +18263,6 @@ }, "wrap-ansi": { "version": "7.0.0", - "devOptional": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -16642,15 +18310,13 @@ "dev": true }, "y18n": { - "version": "5.0.8", - "devOptional": true + "version": "5.0.8" }, "yallist": { "version": "4.0.0" }, "yargs": { "version": "16.2.0", - "devOptional": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -16662,8 +18328,7 @@ } }, "yargs-parser": { - "version": "20.2.9", - "devOptional": true + "version": "20.2.9" }, "yauzl": { "version": "2.10.0", diff --git a/backend/package.json b/backend/package.json index 4ea4dd469..354100ee4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,8 +30,11 @@ "joi": "17.6.0", "lodash": "4.17.21", "lru-cache": "7.10.1", + "mjml": "4.13.0", "mongodb": "4.4.0", + "mustache": "4.2.0", "node-fetch": "2.6.7", + "nodemailer": "6.9.1", "nodemon": "2.0.17", "object-hash": "3.0.0", "path": "0.12.7", @@ -53,8 +56,10 @@ "@types/ioredis": "4.28.10", "@types/jest": "27.5.0", "@types/lodash": "4.14.178", + "@types/mustache": "4.2.2", "@types/node": "17.0.18", "@types/node-fetch": "2.6.1", + "@types/nodemailer": "6.4.7", "@types/object-hash": "2.2.1", "@types/supertest": "2.0.12", "@types/swagger-stats": "0.95.4", diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 10efdc6db..afd75ca2f 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -19,6 +19,7 @@ import * as RedisClient from "../../init/redis"; import { v4 as uuidv4 } from "uuid"; import { ObjectId } from "mongodb"; import * as ReportDAL from "../../dal/report"; +import emailQueue from "../../queues/email-queue"; async function verifyCaptcha(captcha: string): Promise { if (!(await verify(captcha))) { @@ -58,6 +59,59 @@ export async function createNewUser( return new MonkeyResponse("User created"); } +export async function sendVerificationEmail( + req: MonkeyTypes.Request +): Promise { + const { email, uid } = req.ctx.decodedToken; + const isVerified = (await admin.auth().getUser(uid)).emailVerified; + if (isVerified === true) { + throw new MonkeyError(400, "Email already verified"); + } + + const userInfo = await UserDAL.getUser(uid, "request verification email"); + + const link = await admin.auth().generateEmailVerificationLink(email, { + url: + process.env.MODE === "dev" + ? "http://localhost:3000" + : "https://monkeytype.com", + }); + await emailQueue.sendVerificationEmail(email, userInfo.name, link); + + return new MonkeyResponse("Email sent"); +} + +export async function sendForgotPasswordEmail( + req: MonkeyTypes.Request +): Promise { + const { email } = req.body; + + let auth; + try { + auth = await admin.auth().getUserByEmail(email); + } catch (e) { + if (e.code === "auth/user-not-found") { + throw new MonkeyError(404, "User not found"); + } + throw e; + } + + const userInfo = await UserDAL.getUser( + auth.uid, + "request forgot password email" + ); + + const link = await admin.auth().generatePasswordResetLink(email, { + url: + process.env.MODE === "dev" + ? "http://localhost:3000" + : "https://monkeytype.com", + }); + await emailQueue.sendForgotPasswordEmail(email, userInfo.name, link); + + return new MonkeyResponse("Email sent if user was found"); +} + export async function deleteUser( req: MonkeyTypes.Request ): Promise { diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index 3d792f4b3..7ef805861 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -582,4 +582,22 @@ router.post( asyncHandler(UserController.reportUser) ); +router.get( + "/verificationEmail", + authenticateRequest(), + RateLimit.userRequestVerificationEmail, + asyncHandler(UserController.sendVerificationEmail) +); + +router.post( + "/forgotPasswordEmail", + RateLimit.userForgotPasswordEmail, + validateRequest({ + body: { + email: joi.string().email().required(), + }, + }), + asyncHandler(UserController.sendForgotPasswordEmail) +); + export default router; diff --git a/backend/src/init/email-client.ts b/backend/src/init/email-client.ts new file mode 100644 index 000000000..2aa1cfcf6 --- /dev/null +++ b/backend/src/init/email-client.ts @@ -0,0 +1,141 @@ +import * as nodemailer from "nodemailer"; +import Logger from "../utils/logger"; +import fs from "fs"; +import { join } from "path"; +import mjml2html from "mjml"; +import mustache from "mustache"; +import { recordEmail } from "../utils/prometheus"; +import { EmailTaskContexts, EmailType } from "../queues/email-queue"; + +interface EmailMetadata { + subject: string; + templateName: string; +} + +const templates: Record = { + verify: { + subject: "Verify your Monkeytype account", + templateName: "verification.html", + }, + resetPassword: { + subject: "Reset your Monkeytype password", + templateName: "reset-password.html", + }, +}; + +let transportInitialized = false; +let transporter: nodemailer.Transporter; + +export function isInitialized(): boolean { + return transportInitialized; +} + +export async function init(): Promise { + if (isInitialized()) { + return; + } + + const { EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT, MODE } = process.env; + + if (!EMAIL_HOST || !EMAIL_USER || !EMAIL_PASS) { + if (MODE === "dev") { + Logger.warning( + "No email client configuration provided. Running without email." + ); + return; + } + throw new Error("No email client configuration provided"); + } + + try { + transporter = nodemailer.createTransport({ + host: EMAIL_HOST, + secure: EMAIL_PORT === "465" ? true : false, + port: parseInt(EMAIL_PORT ?? "578", 10), + auth: { + user: EMAIL_USER, + pass: EMAIL_PASS, + }, + }); + transportInitialized = true; + + Logger.info("Verifying email client configuration..."); + const result = await transporter.verify(); + + if (result !== true) { + throw new Error( + `Could not verify email client configuration: ` + JSON.stringify(result) + ); + } + + Logger.success("Email client configuration verified"); + } catch (error) { + transportInitialized = false; + Logger.error(error.message); + Logger.error("Failed to verify email client configuration."); + } +} + +interface MailResult { + success: boolean; + message: string; +} + +export async function sendEmail( + templateName: EmailType, + to: string, + data: EmailTaskContexts[M] +): Promise { + if (!isInitialized()) { + return { + success: false, + message: "Email client transport not initialized", + }; + } + + const template = await fillTemplate(templateName, data); + + const mailOptions = { + from: "Monkeytype ", + to, + subject: templates[templateName].subject, + html: template, + }; + + const result = await transporter.sendMail(mailOptions); + + recordEmail(templateName, result.accepted.length === 0 ? "fail" : "success"); + + return { + success: result.accepted.length !== 0, + message: result.response, + }; +} + +const EMAIL_TEMPLATES_DIRECTORY = join(__dirname, "../../email-templates"); + +const cachedTemplates: Record = {}; + +async function getTemplate(name: string): Promise { + if (cachedTemplates[name]) { + return cachedTemplates[name]; + } + + const template = await fs.promises.readFile( + `${EMAIL_TEMPLATES_DIRECTORY}/${name}`, + "utf-8" + ); + + const html = mjml2html(template).html; + + cachedTemplates[name] = html; + return html; +} + +async function fillTemplate( + type: M, + data: EmailTaskContexts[M] +): Promise { + const template = await getTemplate(templates[type].templateName); + return mustache.render(template, data); +} diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts index b6da73440..8c4afbac5 100644 --- a/backend/src/middlewares/rate-limit.ts +++ b/backend/src/middlewares/rate-limit.ts @@ -441,6 +441,20 @@ export const userDiscordUnlink = rateLimit({ handler: customHandler, }); +export const userRequestVerificationEmail = rateLimit({ + windowMs: ONE_HOUR_MS / 4, + max: 1 * REQUEST_MULTIPLIER, + keyGenerator: getKeyWithUid, + handler: customHandler, +}); + +export const userForgotPasswordEmail = rateLimit({ + windowMs: ONE_HOUR_MS / 4, + max: 1 * REQUEST_MULTIPLIER, + keyGenerator: getKeyWithUid, + handler: customHandler, +}); + export const userProfileGet = rateLimit({ windowMs: ONE_HOUR_MS, max: 100 * REQUEST_MULTIPLIER, diff --git a/backend/src/queues/email-queue.ts b/backend/src/queues/email-queue.ts new file mode 100644 index 000000000..fa9d7b74a --- /dev/null +++ b/backend/src/queues/email-queue.ts @@ -0,0 +1,68 @@ +import { MonkeyQueue } from "./monkey-queue"; + +const QUEUE_NAME = "email-tasks"; + +export type EmailType = "verify" | "resetPassword"; + +export interface EmailTask { + type: M; + email: string; + ctx: EmailTaskContexts[M]; +} + +export type EmailTaskContexts = { + verify: { + name: string; + verificationLink: string; + }; + resetPassword: { + name: string; + passwordResetLink: string; + }; +}; + +function buildTask( + taskName: EmailType, + email: string, + taskContext: EmailTaskContexts[EmailType] +): EmailTask { + return { + type: taskName, + email: email, + ctx: taskContext, + }; +} + +class EmailQueue extends MonkeyQueue> { + async sendVerificationEmail( + email: string, + name: string, + verificationLink: string + ): Promise { + const taskName = "verify"; + const task = buildTask(taskName, email, { name, verificationLink }); + await this.add(taskName, task); + } + + async sendForgotPasswordEmail( + email: string, + name: string, + passwordResetLink: string + ): Promise { + const taskName = "resetPassword"; + const task = buildTask(taskName, email, { name, passwordResetLink }); + await this.add(taskName, task); + } +} + +export default new EmailQueue(QUEUE_NAME, { + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + backoff: { + type: "exponential", + delay: 2000, + }, + }, +}); diff --git a/backend/src/queues/index.ts b/backend/src/queues/index.ts index 9c4dbbda5..7cd78b61b 100644 --- a/backend/src/queues/index.ts +++ b/backend/src/queues/index.ts @@ -1,4 +1,5 @@ import LaterQueue from "./later-queue"; import GeorgeQueue from "./george-queue"; +import EmailQueue from "./email-queue"; -export default [GeorgeQueue, LaterQueue]; +export default [GeorgeQueue, LaterQueue, EmailQueue]; diff --git a/backend/src/server.ts b/backend/src/server.ts index 34ad8e9e6..a5207785b 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -14,6 +14,7 @@ import * as RedisClient from "./init/redis"; import queues from "./queues"; import workers from "./workers"; import Logger from "./utils/logger"; +import * as EmailClient from "./init/email-client"; async function bootServer(port: number): Promise { try { @@ -35,6 +36,9 @@ async function bootServer(port: number): Promise { const liveConfiguration = await getLiveConfiguration(); Logger.success("Live configuration fetched"); + Logger.info("Initializing email client..."); + EmailClient.init(); + Logger.info("Connecting to redis..."); await RedisClient.connect(); @@ -46,13 +50,21 @@ async function bootServer(port: number): Promise { queues.forEach((queue) => { queue.init(connection); }); - Logger.success("Queues initialized"); + Logger.success( + `Queues initialized: ${queues + .map((queue) => queue.queueName) + .join(", ")}` + ); Logger.info("Initializing workers..."); workers.forEach((worker) => { worker(connection).run(); }); - Logger.success("Workers initialized"); + Logger.success( + `Workers initialized: ${workers + .map((worker) => worker(connection).name) + .join(", ")}` + ); } initializeDailyLeaderboardsCache(liveConfiguration.dailyLeaderboards); diff --git a/backend/src/utils/prometheus.ts b/backend/src/utils/prometheus.ts index 0f8e0b4bd..17c250ab7 100644 --- a/backend/src/utils/prometheus.ts +++ b/backend/src/utils/prometheus.ts @@ -292,6 +292,16 @@ export function setQueueLength( queueLength.set({ queueName, countType }, length); } +const emailCount = new Counter({ + name: "email_count", + help: "Emails sent by the server", + labelNames: ["type", "status"], +}); + +export function recordEmail(type: string, status: string): void { + emailCount.inc({ type, status }); +} + const timeToCompleteJobTotal = new Counter({ name: "time_to_complete_job_total", help: "Time to complete a job total", diff --git a/backend/src/workers/email-worker.ts b/backend/src/workers/email-worker.ts new file mode 100644 index 000000000..df3bcc7fa --- /dev/null +++ b/backend/src/workers/email-worker.ts @@ -0,0 +1,36 @@ +import _ from "lodash"; +import IORedis from "ioredis"; +import { Worker, Job } from "bullmq"; +import Logger from "../utils/logger"; +import EmailQueue, { + EmailTaskContexts, + EmailType, +} from "../queues/email-queue"; +import { sendEmail } from "../init/email-client"; +import { recordTimeToCompleteJob } from "../utils/prometheus"; + +async function jobHandler(job: Job): Promise { + const type: EmailType = job.data.type; + const email: string = job.data.email; + const ctx: EmailTaskContexts[typeof type] = job.data.ctx; + + Logger.info(`Starting job: ${type}`); + + const start = performance.now(); + + const result = await sendEmail(type, email, ctx); + + if (!result.success) { + throw new Error(result.message); + } + + const elapsed = performance.now() - start; + recordTimeToCompleteJob(EmailQueue.queueName, type, elapsed); + Logger.success(`Job: ${type} - completed in ${elapsed}ms`); +} + +export default (redisConnection?: IORedis.Redis): Worker => + new Worker(EmailQueue.queueName, jobHandler, { + autorun: false, + connection: redisConnection, + }); diff --git a/backend/src/workers/index.ts b/backend/src/workers/index.ts index 610ec79d4..ae49644ad 100644 --- a/backend/src/workers/index.ts +++ b/backend/src/workers/index.ts @@ -1,3 +1,4 @@ import LaterWorker from "./later-worker"; +import EmailWorker from "./email-worker"; -export default [LaterWorker]; +export default [LaterWorker, EmailWorker]; diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index afeafa3d0..90b8e42d6 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -241,4 +241,14 @@ export default class Users { return await this.httpClient.post(`${BASE_PATH}/report`, { payload }); } + + async verificationEmail(): Ape.EndpointData { + return await this.httpClient.get(`${BASE_PATH}/verificationEmail`); + } + + async forgotPasswordEmail(email: string): Ape.EndpointData { + return await this.httpClient.post(`${BASE_PATH}/forgotPasswordEmail`, { + payload: { email }, + }); + } } diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index 9a33b0413..b4fb6612d 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -26,7 +26,6 @@ import { browserSessionPersistence, browserLocalPersistence, createUserWithEmailAndPassword, - sendEmailVerification, signInWithEmailAndPassword, signInWithPopup, setPersistence, @@ -35,7 +34,6 @@ import { linkWithCredential, reauthenticateWithPopup, getAdditionalUserInfo, - sendPasswordResetEmail, User as UserType, Unsubscribe, } from "firebase/auth"; @@ -50,26 +48,25 @@ import { update as updateTagsCommands } from "../commandline/lists/tags"; import * as ConnectionState from "../states/connection"; export const gmailProvider = new GoogleAuthProvider(); -let canCall = true; -export function sendVerificationEmail(): void { +export async function sendVerificationEmail(): Promise { if (Auth === undefined) { Notifications.add("Authentication uninitialized", -1, 3); return; } + Loader.show(); - const user = Auth.currentUser; - if (user === null) return; - sendEmailVerification(user) - .then(() => { - Loader.hide(); - Notifications.add("Email sent to " + user.email, 4000); - }) - .catch((e) => { - Loader.hide(); - Notifications.add("Error: " + e.message, 3000); - console.error(e.message); - }); + const result = await Ape.users.verificationEmail(); + if (result.status !== 200) { + Loader.hide(); + Notifications.add( + "Failed to request verification email: " + result.message, + 3000 + ); + } else { + Loader.hide(); + Notifications.add("Verification email sent", 1, 3); + } } export async function getDataAndInit(): Promise { @@ -241,12 +238,6 @@ export async function getDataAndInit(): Promise { export async function loadUser(user: UserType): Promise { // User is signed in. - $(".pageAccount .content p.accountVerificatinNotice").remove(); - if (user.emailVerified === false) { - $(".pageAccount .content").prepend( - `

Your account is not verified. Send the verification email again.` - ); - } PageTransition.set(false); AccountButton.loading(true); if ((await getDataAndInit()) === false) { @@ -413,35 +404,6 @@ export async function signIn(): Promise { }); } -export async function forgotPassword(email: any): Promise { - if (Auth === undefined) { - Notifications.add("Authentication uninitialized", -1, 3); - return; - } - if (!canCall) { - return Notifications.add( - "Please wait before requesting another password reset link", - 0, - 5000 - ); - } - if (!email) return Notifications.add("Please enter an email!", -1); - - try { - await sendPasswordResetEmail(Auth, email); - Notifications.add("Email sent", 1, 2); - } catch (error) { - Notifications.add( - Misc.createErrorMessage(error, "Failed to send email"), - -1 - ); - } - canCall = false; - setTimeout(function () { - canCall = true; - }, 10000); -} - export async function signInWithGoogle(): Promise { if (Auth === undefined) { Notifications.add("Authentication uninitialized", -1, 3); @@ -676,7 +638,7 @@ async function signUp(): Promise { } await updateProfile(createdAuthUser.user, { displayName: nname }); - await sendEmailVerification(createdAuthUser.user); + await sendVerificationEmail(); AllTimeStats.clear(); $("#menu .textButton.account .text").text(nname); $(".pageLogin .button").removeClass("disabled"); @@ -725,13 +687,6 @@ async function signUp(): Promise { } } -$(".pageLogin #forgotPasswordButton").on("click", () => { - const emailField = - ($(".pageLogin .login input")[0] as HTMLInputElement).value || ""; - const email = prompt("Email address", emailField); - forgotPassword(email); -}); - $(".pageLogin .login input").keyup((e) => { if (e.key === "Enter") { UpdateConfig.setChangedBeforeDb(false); diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 79c22edad..a3231dca3 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -17,6 +17,7 @@ import format from "date-fns/format"; import * as ConnectionState from "../states/connection"; import * as Skeleton from "../popups/skeleton"; import type { ScaleChartOptions } from "chart.js"; +import { Auth } from "../firebase"; let filterDebug = false; //toggle filterdebug @@ -1278,6 +1279,12 @@ export const page = new Page( await update(); await Misc.sleep(0); updateChartColors(); + $(".pageAccount .content p.accountVerificatinNotice").remove(); + if (Auth?.currentUser?.emailVerified === false) { + $(".pageAccount .content").prepend( + `

Your account is not verified. Send the verification email again.` + ); + } }, async () => { // diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts index d3e5054b8..f72b5f486 100644 --- a/frontend/src/ts/popups/simple-popups.ts +++ b/frontend/src/ts/popups/simple-popups.ts @@ -1241,6 +1241,50 @@ list["deleteCustomTheme"] = new SimplePopup( } ); +list["forgotPassword"] = new SimplePopup( + "forgotPassword", + "text", + "Forgot Password", + [ + { + type: "text", + placeholder: "Email", + initVal: "", + }, + ], + "", + "Send", + async (_thisPopup, email) => { + Loader.show(); + const result = await Ape.users.forgotPasswordEmail(email); + if (result.status !== 200) { + Loader.hide(); + Notifications.add( + "Failed to request password reset email: " + result.message, + 5000 + ); + } else { + Loader.hide(); + Notifications.add("Password reset email sent", 1, 3); + } + }, + (thisPopup) => { + const inputValue = $( + `.pageLogin .login input[name="current-email"]` + ).val() as string; + if (inputValue) { + thisPopup.inputs[0].initVal = inputValue; + } + }, + () => { + // + } +); + +$(".pageLogin #forgotPasswordButton").on("click", () => { + list["forgotPassword"].show(); +}); + $(".pageSettings .section.discordIntegration #unlinkDiscordButton").on( "click", () => {