diff --git a/migrations/archive/2020/20200402_webhooks_add_protocol.js b/migrations/archive/2020/20200402_webhooks_add_protocol.js new file mode 100644 index 0000000000..51db53ead5 --- /dev/null +++ b/migrations/archive/2020/20200402_webhooks_add_protocol.js @@ -0,0 +1,69 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20200402_webhooks_add_protocol'; +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + const set = { + migration: MIGRATION_NAME, + }; + + if (user && user.webhooks && user.webhooks.length > 0) { + user.webhooks.forEach(webhook => { + // Make sure the protocol is set and valid + if (webhook.url.startsWith('ftp')) { + webhook.url = webhook.url.replace('ftp', 'https'); + } + + if (!webhook.url.startsWith('http://') && !webhook.url.startsWith('https://')) { + // the default in got 9 was https + // see https://github.com/sindresorhus/got/commit/92bc8082137d7d085750359bbd76c801e213d7d2#diff-0730bb7c2e8f9ea2438b52e419dd86c9L111 + webhook.url = `https://${webhook.url}`; + } + }); + + set.webhooks = user.webhooks; + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return await User.update({ _id: user._id }, { $set: set }).exec(); +} + +module.exports = async function processUsers () { + let query = { + migration: { $ne: MIGRATION_NAME }, + webhooks: { $exists: true, $not: { $size: 0 } }, + }; + + const fields = { + _id: 1, + webhooks: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const users = await User // eslint-disable-line no-await-in-loop + .find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .lean() + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1]._id, + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/migrations/archive/2020/20200402_webhooks_reenable.js b/migrations/archive/2020/20200402_webhooks_reenable.js new file mode 100644 index 0000000000..a34b60babc --- /dev/null +++ b/migrations/archive/2020/20200402_webhooks_reenable.js @@ -0,0 +1,63 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20200402_webhooks_reenable'; +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + const set = { + migration: MIGRATION_NAME, + }; + + if (user && user.webhooks && user.webhooks.length > 0) { + user.webhooks.forEach(webhook => { + // Re-enable webhooks disabled because of too many failures + if (webhook.enabled === false && webhook.lastFailureAt === null) { + webhook.enabled = true; + } + }); + + set.webhooks = user.webhooks; + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return await User.update({ _id: user._id }, { $set: set }).exec(); +} + +module.exports = async function processUsers () { + let query = { + migration: { $ne: MIGRATION_NAME }, + webhooks: { $exists: true, $not: { $size: 0 } }, + }; + + const fields = { + _id: 1, + webhooks: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const users = await User // eslint-disable-line no-await-in-loop + .find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .lean() + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1]._id, + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/package-lock.json b/package-lock.json index b95453d54e..2b27303e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1360,9 +1360,9 @@ } }, "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.0.tgz", + "integrity": "sha512-lXKXfypKo644k4Da4yXkPCrwcvn6SlUW2X2zFbuflKHNjf0w9htru01bo26uMhleMXsDmnZ12eJLdrAZa9MANg==" }, "@sinonjs/commons": { "version": "1.6.0", @@ -1479,11 +1479,22 @@ } }, "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", "requires": { - "defer-to-connect": "^1.0.1" + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" } }, "@types/color-name": { @@ -1507,6 +1518,19 @@ "@types/node": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -1523,6 +1547,14 @@ "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", "optional": true }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3134,18 +3166,27 @@ "unset-value": "^1.0.0" } }, + "cacheable-lookup": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz", + "integrity": "sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg==", + "requires": { + "@types/keyv": "^3.1.1", + "keyv": "^4.0.0" + } + }, "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", "requires": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", + "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "responselike": "^2.0.0" }, "dependencies": { "get-stream": { @@ -3160,6 +3201,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } } } }, @@ -4183,6 +4232,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -4360,9 +4410,9 @@ "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=" }, "defer-to-connect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.2.tgz", - "integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" }, "define-properties": { "version": "1.1.3", @@ -6754,6 +6804,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "optional": true, "requires": { "pump": "^3.0.0" } @@ -7024,21 +7075,82 @@ } }, "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/got/-/got-10.7.0.tgz", + "integrity": "sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg==", "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", + "@sindresorhus/is": "^2.0.0", + "@szmarczak/http-timer": "^4.0.0", + "@types/cacheable-request": "^6.0.1", + "cacheable-lookup": "^2.0.0", + "cacheable-request": "^7.0.1", + "decompress-response": "^5.0.0", "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" + "get-stream": "^5.0.0", + "lowercase-keys": "^2.0.0", + "mimic-response": "^2.1.0", + "p-cancelable": "^2.0.0", + "p-event": "^4.0.0", + "responselike": "^2.0.0", + "to-readable-stream": "^2.0.0", + "type-fest": "^0.10.0" + }, + "dependencies": { + "decompress-response": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz", + "integrity": "sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, + "p-event": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", + "integrity": "sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA==", + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "requires": { + "p-finally": "^1.0.0" + } + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "type-fest": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz", + "integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==" + } } }, "graceful-fs": { @@ -7575,9 +7687,9 @@ } }, "http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-errors": { "version": "1.7.2", @@ -8681,7 +8793,8 @@ "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "optional": true }, "json-content-demux": { "version": "0.1.3", @@ -8825,11 +8938,18 @@ } }, "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.0.tgz", + "integrity": "sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog==", "requires": { - "json-buffer": "3.0.0" + "json-buffer": "3.0.1" + }, + "dependencies": { + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + } } }, "kind-of": { @@ -10707,9 +10827,9 @@ } }, "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" }, "p-defer": { "version": "1.0.0", @@ -11254,7 +11374,8 @@ "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "optional": true }, "pretty-bytes": { "version": "5.3.0", @@ -11867,6 +11988,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "optional": true, "requires": { "lowercase-keys": "^1.0.0" } @@ -13177,9 +13299,9 @@ } }, "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz", + "integrity": "sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==" }, "to-regex": { "version": "3.0.2", @@ -13783,6 +13905,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "optional": true, "requires": { "prepend-http": "^2.0.0" } diff --git a/package.json b/package.json index e79f6e100d..0366902be0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "express-basic-auth": "^1.1.5", "express-validator": "^5.2.0", "glob": "^7.1.6", - "got": "^9.0.0", + "got": "^10.7.0", "gulp": "^4.0.0", "gulp-babel": "^8.0.0", "gulp-imagemin": "^6.2.0", diff --git a/test/api/unit/libs/email.test.js b/test/api/unit/libs/email.test.js index 057766eddd..e518adc0d3 100644 --- a/test/api/unit/libs/email.test.js +++ b/test/api/unit/libs/email.test.js @@ -121,8 +121,7 @@ describe('emails', () => { sendTxnEmail(mailingInfo, emailType); expect(got.post).to.be.calledWith('undefined/job', sinon.match({ - json: true, - body: { + json: { data: { emailType: sinon.match.same(emailType), to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'), @@ -154,8 +153,7 @@ describe('emails', () => { sendTxnEmail(mailingInfo, emailType); expect(got.post).to.be.calledWith('undefined/job', sinon.match({ - json: true, - body: { + json: { data: { emailType: sinon.match.same(emailType), to: sinon.match(val => val[0]._id === mailingInfo._id), @@ -177,8 +175,7 @@ describe('emails', () => { sendTxnEmail(mailingInfo, emailType, variables); expect(got.post).to.be.calledWith('undefined/job', sinon.match({ - json: true, - body: { + json: { data: { variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'), personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email diff --git a/test/api/unit/libs/language.test.js b/test/api/unit/libs/language.test.js new file mode 100644 index 0000000000..ec2df2ced2 --- /dev/null +++ b/test/api/unit/libs/language.test.js @@ -0,0 +1,111 @@ +import { + getLanguageFromBrowser, + getLanguageFromUser, +} from '../../../../website/server/libs/language'; +import { + generateReq, +} from '../../../helpers/api-unit.helper'; + +describe('language lib', () => { + let req; + + beforeEach(() => { + req = generateReq(); + }); + + describe('getLanguageFromUser', () => { + it('uses the user preferred language if avalaible', () => { + const user = { + preferences: { + language: 'it', + }, + }; + + expect(getLanguageFromUser(user, req)).to.equal('it'); + }); + + it('falls back to english if the user preferred language is not avalaible', () => { + const user = { + preferences: { + language: 'bla', + }, + }; + + expect(getLanguageFromUser(user, req)).to.equal('en'); + }); + }); + + describe('getLanguageFromBrowser', () => { + it('uses browser specificed language', () => { + req.headers['accept-language'] = 'pt'; + + expect(getLanguageFromBrowser(req)).to.equal('pt'); + }); + + it('uses first language in series if browser specifies multiple', () => { + req.headers['accept-language'] = 'he, pt, it'; + + expect(getLanguageFromBrowser(req)).to.equal('he'); + }); + + it('skips invalid lanaguages and uses first language in series if browser specifies multiple', () => { + req.headers['accept-language'] = 'blah, he, pt, it'; + + expect(getLanguageFromBrowser(req)).to.equal('he'); + }); + + it('uses normal version of language if specialized locale is passed in', () => { + req.headers['accept-language'] = 'fr-CA'; + + expect(getLanguageFromBrowser(req)).to.equal('fr'); + }); + + it('uses normal version of language if specialized locale is passed in', () => { + req.headers['accept-language'] = 'fr-CA'; + + expect(getLanguageFromBrowser(req)).to.equal('fr'); + }); + + it('uses es if es is passed in', () => { + req.headers['accept-language'] = 'es'; + + expect(getLanguageFromBrowser(req)).to.equal('es'); + }); + + it('uses es_419 if applicable es-languages are passed in', () => { + req.headers['accept-language'] = 'es-mx'; + + expect(getLanguageFromBrowser(req)).to.equal('es_419'); + }); + + it('uses es_419 if multiple es languages are passed in', () => { + req.headers['accept-language'] = 'es-GT, es-MX, es-CR'; + + expect(getLanguageFromBrowser(req)).to.equal('es_419'); + }); + + it('zh', () => { + req.headers['accept-language'] = 'zh-TW'; + + expect(getLanguageFromBrowser(req)).to.equal('zh_TW'); + }); + + it('uses english if browser specified language is not compatible', () => { + req.headers['accept-language'] = 'blah'; + + expect(getLanguageFromBrowser(req)).to.equal('en'); + }); + + it('uses english if browser does not specify', () => { + req.headers['accept-language'] = ''; + + expect(getLanguageFromBrowser(req)).to.equal('en'); + }); + + it('uses english if browser does not supply an accept-language header', () => { + delete req.headers['accept-language']; + + expect(getLanguageFromBrowser(req)).to.equal('en'); + }); + }); +}); diff --git a/test/api/unit/libs/webhooks.test.js b/test/api/unit/libs/webhooks.test.js index 1715e99650..47f44cdc66 100644 --- a/test/api/unit/libs/webhooks.test.js +++ b/test/api/unit/libs/webhooks.test.js @@ -101,8 +101,7 @@ describe('webhooks', () => { expect(WebhookSender.defaultTransformData).to.be.calledOnce; expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - json: true, - body, + json: body, }); }); @@ -122,7 +121,7 @@ describe('webhooks', () => { expect(sendWebhook.attachDefaultData).to.be.calledOnce; expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - json: true, + json: body, }); expect(body).to.eql({ @@ -153,8 +152,7 @@ describe('webhooks', () => { expect(WebhookSender.defaultTransformData).to.not.be.called; expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - json: true, - body: { + json: { foo: 'bar', baz: 'biz', }, @@ -271,8 +269,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - body, - json: true, + json: body, }); }); @@ -292,8 +289,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - body, - json: true, + json: body, }); }); @@ -316,12 +312,10 @@ describe('webhooks', () => { expect(got.post).to.be.calledTwice; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - body, - json: true, + json: body, }); expect(got.post).to.be.calledWithMatch('http://other-url.com', { - body, - json: true, + json: body, }); }); @@ -351,8 +345,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - json: true, - body, + json: body, }); await sleep(0.1); @@ -368,8 +361,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://custom-url.com', { - json: true, - body, + json: body, }); await sleep(0.1); @@ -459,8 +451,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[0].url, { - json: true, - body: { + json: { type: 'scored', webhookType: 'taskActivity', user: { @@ -497,8 +488,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch('http://global-activity.com', { - json: true, - body: { + json: { type: 'scored', webhookType: 'taskActivity', user: { @@ -551,8 +541,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[0].url, { - json: true, - body: { + json: { type, webhookType: 'taskActivity', user: { @@ -592,8 +581,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[0].url, { - json: true, - body: { + json: { webhookType: 'taskActivity', user: { _id: user._id, @@ -633,8 +621,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[2].url, { - json: true, - body: { + json: { type, webhookType: 'userActivity', user: { @@ -680,8 +667,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[1].url, { - json: true, - body: { + json: { type, webhookType: 'questActivity', user: { @@ -727,8 +713,7 @@ describe('webhooks', () => { expect(got.post).to.be.calledOnce; expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, { - json: true, - body: { + json: { webhookType: 'groupChatReceived', user: { _id: user._id, diff --git a/test/api/unit/middlewares/analytics.test.js b/test/api/unit/middlewares/analytics.test.js index 32b88d00b8..8ce68aee73 100644 --- a/test/api/unit/middlewares/analytics.test.js +++ b/test/api/unit/middlewares/analytics.test.js @@ -19,7 +19,7 @@ describe('analytics middleware', () => { next = generateNext(); }); - it('attaches analytics object res.locals', () => { + it('attaches analytics object to res', () => { const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default; attachAnalytics(req, res, next); diff --git a/test/api/unit/middlewares/cronMiddleware.js b/test/api/unit/middlewares/cronMiddleware.js index bd93b06e65..873bbb70a7 100644 --- a/test/api/unit/middlewares/cronMiddleware.js +++ b/test/api/unit/middlewares/cronMiddleware.js @@ -21,28 +21,11 @@ describe('cron middleware', () => { req; let user; - beforeEach(done => { + beforeEach(async () => { res = generateRes(); req = generateReq(); - user = new User({ - auth: { - local: { - username: 'username', - lowerCaseUsername: 'username', - email: 'email@email.email', - salt: 'salt', - hashed_password: 'hashed_password', // eslint-disable-line camelcase - }, - }, - }); - - user.save() - .then(savedUser => { - res.locals.user = savedUser; - res.analytics = analyticsService; - done(); - }) - .catch(done); + user = await res.locals.user.save(); + res.analytics = analyticsService; }); afterEach(() => { diff --git a/test/api/unit/middlewares/language.test.js b/test/api/unit/middlewares/language.test.js index 4965da9a49..8aad89e714 100644 --- a/test/api/unit/middlewares/language.test.js +++ b/test/api/unit/middlewares/language.test.js @@ -12,6 +12,9 @@ import { model as User } from '../../../../website/server/models/user'; const { i18n } = common; +// TODO some of the checks here can be simplified to simply check +// that the right parameters are passed to the functions in libs/language + describe('language middleware', () => { describe('res.t', () => { let res; let req; let @@ -19,6 +22,8 @@ describe('language middleware', () => { beforeEach(() => { res = generateRes(); + // remove the defaul user + res.locals.user = undefined; req = generateReq(); next = generateNext(); @@ -57,6 +62,8 @@ describe('language middleware', () => { beforeEach(() => { res = generateRes(); + // remove the defaul user + res.locals.user = undefined; req = generateReq(); next = generateNext(); attachTranslateFunction(req, res, next); @@ -88,7 +95,7 @@ describe('language middleware', () => { lang: 'es', }; - req.locals = { + res.locals = { user: { preferences: { language: 'it', @@ -108,7 +115,7 @@ describe('language middleware', () => { context('authorized request', () => { it('uses the user preferred language if avalaible', () => { - req.locals = { + res.locals = { user: { preferences: { language: 'it', @@ -122,7 +129,7 @@ describe('language middleware', () => { }); it('falls back to english if the user preferred language is not avalaible', done => { - req.locals = { + res.locals = { user: { preferences: { language: 'bla', @@ -138,7 +145,7 @@ describe('language middleware', () => { }); it('uses the user preferred language even if a session is included in request', () => { - req.locals = { + res.locals = { user: { preferences: { language: 'it', diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 5b386e7a7f..ff1a6ec44b 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -33,7 +33,7 @@ 'resting': showRestingBanner }" > - + @@ -266,7 +266,6 @@ import { } from '@/libs/userlocalManager'; import svgClose from '@/assets/svg/close.svg'; -import bannedAccountModal from '@/components/bannedAccountModal'; const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line @@ -281,7 +280,6 @@ export default { BuyModal, SelectMembersModal, amazonPaymentsModal, - bannedAccountModal, paymentsSuccessModal, subCancelModalConfirm, subCanceledModal, @@ -385,7 +383,8 @@ export default { return response; }, error => { if (error.response.status >= 400) { - this.checkForBannedUser(error); + const isBanned = this.checkForBannedUser(error); + if (isBanned === true) return null; // eslint-disable-line consistent-return // Don't show errors from getting user details. These users have delete their account, // but their chat message still exists. @@ -403,7 +402,8 @@ export default { // TODO use a specific error like NotificationNotFound instead of checking for the string const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; if (invalidUserMessage.indexOf(errorMessage) !== -1) { - this.$store.dispatch('auth:logout'); + this.$store.dispatch('auth:logout', { redirectToLogin: true }); + return null; } // Most server errors should return is click to dismiss errors, with some exceptions @@ -553,7 +553,7 @@ export default { // Case where user is not logged in if (!parseSettings) { - return; + return false; } const bannedMessage = this.$t('accountSuspended', { @@ -561,9 +561,10 @@ export default { userId: parseSettings.auth.apiId, }); - if (errorMessage !== bannedMessage) return; + if (errorMessage !== bannedMessage) return false; - this.$root.$emit('bv::show::modal', 'banned-account'); + this.$store.dispatch('auth:logout', { redirectToLogin: true }); + return true; }, initializeModalStack () { // Manage modals diff --git a/website/client/src/components/avatar.vue b/website/client/src/components/avatar.vue index 93609b7dd8..3ca2483d3d 100644 --- a/website/client/src/components/avatar.vue +++ b/website/client/src/components/avatar.vue @@ -186,7 +186,7 @@ export default { return this.overrideTopPadding; } - let val = '27px'; + let val = '24px'; if (!this.avatarOnly) { if (this.member.items.currentPet) val = '24px'; diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index d10ecef08a..af8e740609 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -348,6 +348,7 @@ > +
-

diff --git a/website/client/src/store/actions/auth.js b/website/client/src/store/actions/auth.js index af909201b8..8d80da0ecd 100644 --- a/website/client/src/store/actions/auth.js +++ b/website/client/src/store/actions/auth.js @@ -82,7 +82,8 @@ export async function socialAuth (store, params) { localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData); } -export function logout () { +export function logout (store, options = {}) { localStorage.clear(); - window.location.href = '/logout-server'; + const query = options.redirectToLogin === true ? '?redirectToLogin=true' : ''; + window.location.href = `/logout-server${query}`; } diff --git a/website/client/tests/unit/components/avatar.spec.js b/website/client/tests/unit/components/avatar.spec.js index 49e6f8398d..c1f49322df 100644 --- a/website/client/tests/unit/components/avatar.spec.js +++ b/website/client/tests/unit/components/avatar.spec.js @@ -78,7 +78,7 @@ context('avatar.vue', () => { }; }); - it('defaults to 27px', () => { + xit('defaults to 27px', () => { vm.avatarOnly = true; expect(vm.paddingTop).to.equal('27px'); }); diff --git a/website/common/locales/de/achievements.json b/website/common/locales/de/achievements.json index 3749d44194..7db899f717 100644 --- a/website/common/locales/de/achievements.json +++ b/website/common/locales/de/achievements.json @@ -72,6 +72,8 @@ "achievementTickledPink": "Rosige Bäckchen", "foundNewItems": "Du hast neue Gegenstände gefunden!", "foundNewItemsCTA": "Schau in Dein Inventar und versuche, Dein neues Schlüpfelixier mit einem Ei zu kombinieren!", - "foundNewItemsExplanation": "Durch das Abschließen von Aufgaben erhältst Du die Chance Gegenstände, wie etwa Eier, Schlüpfelixiere und Futter, zu finden.", - "achievementBugBonanza": "Kostbarer Käfer" + "foundNewItemsExplanation": "Durch das Abschließen von Aufgaben erhältst Du die Chance, Gegenstände wie etwa Eier, Schlüpfelixiere und Futter zu finden.", + "achievementBugBonanza": "Kostbarer Käfer", + "achievementBugBonanzaModalText": "Du hast die Käfer-, Schmetterling-, Schnecken- und Spinnenhaustier-Quests erfüllt!", + "achievementBugBonanzaText": "Hat die Käfer-, Schmetterling-, Schnecken- und Spinnenhaustier-Quests erfüllt." } diff --git a/website/common/locales/de/content.json b/website/common/locales/de/content.json index 3881efa776..d366047368 100644 --- a/website/common/locales/de/content.json +++ b/website/common/locales/de/content.json @@ -4,7 +4,7 @@ "armoireText": "Verzauberter Schrank", "armoireNotesFull": "Öffne den Schrank, um zufällig spezielle Ausrüstung, Erfahrung oder Futter zu erhalten! Verbleibende Ausrüstungsgegenstände:", "armoireLastItem": "Du hast das letzte Stück seltener Ausrüstung im verzauberten Schrank gefunden.", - "armoireNotesEmpty": "Im verzauberten Schrank gibt es jeweils in der ersten Woche eines Monats neue Ausrüstung. Bis dahin, klicke weiter für Erfahrung und Futter!", + "armoireNotesEmpty": "Im verzauberten Schrank gibt es jeweils in der ersten Woche des Monats neue Ausrüstung. Bis dahin klicke weiter für Erfahrung und Futter!", "dropEggWolfText": "Wolfsjunges", "dropEggWolfMountText": "Wolfs-Reittier", "dropEggWolfAdjective": "ein treues", @@ -354,5 +354,6 @@ "premiumPotionUnlimitedNotes": "Nicht auf Eier von Quest-Haustieren anwendbar.", "hatchingPotionAmber": "Bernstein", "hatchingPotionAurora": "Polarlicht", - "hatchingPotionRuby": "Rubinrotes" + "hatchingPotionRuby": "Rubinrotes", + "hatchingPotionBirchBark": "Birkenborke" } diff --git a/website/common/locales/de/limited.json b/website/common/locales/de/limited.json index 00d61a18b5..63467bde11 100644 --- a/website/common/locales/de/limited.json +++ b/website/common/locales/de/limited.json @@ -85,45 +85,45 @@ "scarecrowWarriorSet": "Vogelscheuchenkrieger (Krieger)", "stitchWitchSet": "Stichhexe (Magier)", "potionerSet": "Tränkebrauer (Heiler)", - "battleRogueSet": "Kampfschurke (Schurke)", + "battleRogueSet": "Kampf-Fleder (Schurke)", "springingBunnySet": "Hüpfendes Häschen (Heiler)", "grandMalkinSet": "Prächtiger Kater (Magier)", "cleverDogSet": "Schlauer Hund (Schurke)", "braveMouseSet": "Mutige Maus (Krieger)", - "summer2016SharkWarriorSet": "Haifisch-Krieger (Krieger)", - "summer2016DolphinMageSet": "Delfin-Magier (Magier)", - "summer2016SeahorseHealerSet": "Seepferdchen-Heiler (Heiler)", - "summer2016EelSet": "Zitteraal-Schurke (Schurke)", + "summer2016SharkWarriorSet": "Haifisch(Krieger)", + "summer2016DolphinMageSet": "Delfin (Magier)", + "summer2016SeahorseHealerSet": "Seepferdchen (Heiler)", + "summer2016EelSet": "Zitteraal (Schurke)", "fall2016SwampThingSet": "Das Ding aus dem Sumpf (Krieger)", "fall2016WickedSorcererSet": "Boshafter Zauberer (Magier)", - "fall2016GorgonHealerSet": "Gorgonen-Heiler (Heiler)", - "fall2016BlackWidowSet": "Schurkische Schwarze Witwe (Schurke)", + "fall2016GorgonHealerSet": "Gorgone (Heiler)", + "fall2016BlackWidowSet": "Schwarze Witwe (Schurke)", "winter2017IceHockeySet": "Eishockey (Krieger)", "winter2017WinterWolfSet": "Winterwolf (Magier)", - "winter2017SugarPlumSet": "Zuckerpflaumen-Heiler (Heiler)", - "winter2017FrostyRogueSet": "Frostiger Schurke (Schurke)", - "spring2017FelineWarriorSet": "Katzenhafter Krieger (Krieger)", + "winter2017SugarPlumSet": "Zuckerpflaume (Heiler)", + "winter2017FrostyRogueSet": "Frosty (Schurke)", + "spring2017FelineWarriorSet": "Katzenhaft (Krieger)", "spring2017CanineConjurorSet": "Bellender Beschwörer (Magier)", "spring2017FloralMouseSet": "Blumenmaus (Heiler)", "spring2017SneakyBunnySet": "Raffiniertes Häschen (Schurke)", - "summer2017SandcastleWarriorSet": "Sandburg-Krieger (Krieger)", - "summer2017WhirlpoolMageSet": "Whirlpool-Magier (Magier)", + "summer2017SandcastleWarriorSet": "Sandburg (Krieger)", + "summer2017WhirlpoolMageSet": "Whirlpool (Magier)", "summer2017SeashellSeahealerSet": "Muschel-Meeresheiler (Heiler)", "summer2017SeaDragonSet": "Seedrache (Schurke)", - "fall2017HabitoweenSet": "Habitoween-Krieger (Krieger)", - "fall2017MasqueradeSet": "Maskerade-Magier (Magier)", - "fall2017HauntedHouseSet": "Geisterhaus-Heiler (Heiler)", - "fall2017TrickOrTreatSet": "Süßes-oder-Saures-Schurke (Schurke)", - "winter2018ConfettiSet": "Konfettimagier (Magier)", - "winter2018GiftWrappedSet": "Geschenkpapierverpackter Krieger (Krieger)", - "winter2018MistletoeSet": "Mistelzweigheiler (Heiler)", - "winter2018ReindeerSet": "Rentier-Schurke (Schurke)", - "spring2018SunriseWarriorSet": "Sonnenaufgang-Krieger (Krieger)", - "spring2018TulipMageSet": "Tulpenmagier (Magier)", - "spring2018GarnetHealerSet": "Granatheiler (Heiler)", - "spring2018DucklingRogueSet": "Entchen-Schurke (Schurke)", - "summer2018BettaFishWarriorSet": "Kampffisch-Krieger (Krieger)", - "summer2018LionfishMageSet": "Feuerfisch-Magier (Magier)", + "fall2017HabitoweenSet": "Habitoween (Krieger)", + "fall2017MasqueradeSet": "Maskerade (Magier)", + "fall2017HauntedHouseSet": "Geisterhaus (Heiler)", + "fall2017TrickOrTreatSet": "Süßes oder Saures (Schurke)", + "winter2018ConfettiSet": "Konfetti (Magier)", + "winter2018GiftWrappedSet": "Geschenkpapierverpackt (Krieger)", + "winter2018MistletoeSet": "Mistelzweig(Heiler)", + "winter2018ReindeerSet": "Rentier (Schurke)", + "spring2018SunriseWarriorSet": "Sonnenaufgang (Krieger)", + "spring2018TulipMageSet": "Tulpe (Magier)", + "spring2018GarnetHealerSet": "Granat (Heiler)", + "spring2018DucklingRogueSet": "Entchen (Schurke)", + "summer2018BettaFishWarriorSet": "Kampffisch (Krieger)", + "summer2018LionfishMageSet": "Feuerfisch (Magier)", "summer2018MerfolkMonarchSet": "Meervolk-Monarch (Heiler)", "summer2018FisherRogueSet": "Fischdieb (Schurke)", "fall2018MinotaurWarriorSet": "Minotaurus (Krieger)", @@ -173,5 +173,10 @@ "winter2020WinterSpiceSet": "Wintergewürz (Heiler)", "winter2020CarolOfTheMageSet": "Weihnachtslied des Magiers (Magier)", "winter2020EvergreenSet": "Immergrün (Krieger)", - "decemberYYYY": "Dezember <%= year %>" + "decemberYYYY": "Dezember <%= year %>", + "spring2020BeetleWarriorSet": "Nashornkäfer (Krieger)", + "marchYYYY": "März <%= year %>", + "spring2020LapisLazuliRogueSet": "Lapislazuli (Schurke)", + "spring2020IrisHealerSet": "Iris (Heiler)", + "spring2020PuddleMageSet": "Pfütze (Magier)" } diff --git a/website/common/locales/de/subscriber.json b/website/common/locales/de/subscriber.json index a17d466e50..6fcc119ac1 100644 --- a/website/common/locales/de/subscriber.json +++ b/website/common/locales/de/subscriber.json @@ -247,5 +247,6 @@ "subMonths": "Monate abonniert", "subscriptionStats": "Abonnenten-Attributwerte", "doubleDropCap": "Verdopple die Beute", - "mysterySet202003": "Stachliges Streitgewandset" + "mysterySet202003": "Stachliges Streitgewandset", + "mysterySet202004": "Mächtiger-Monarch-Set" } diff --git a/website/common/locales/en/pets.json b/website/common/locales/en/pets.json index e86b0b7fab..228c399d59 100644 --- a/website/common/locales/en/pets.json +++ b/website/common/locales/en/pets.json @@ -46,7 +46,7 @@ "hatchingPotion": "hatching potion", "noHatchingPotions": "You don't have any hatching potions.", "inventoryText": "Click an egg to see usable potions highlighted in green and then click one of the highlighted potions to hatch your pet. If no potions are highlighted, click that egg again to deselect it, and instead click a potion first to have the usable eggs highlighted. You can also sell unwanted drops to Alexander the Merchant.", - "haveHatchablePet": "You have a <%= potion %> hatching potion and <%= egg %> egg to hatch this pet! Click the paw print to hatch.", + "haveHatchablePet": "You have a <%= potion %> hatching potion and <%= egg %> egg to hatch this pet! Click to hatch!", "quickInventory": "Quick Inventory", "food": "Pet Food and Saddles", "noFoodAvailable": "You don't have any Pet Food.", diff --git a/website/common/locales/en_GB/gear.json b/website/common/locales/en_GB/gear.json index 65351741b4..6375013114 100644 --- a/website/common/locales/en_GB/gear.json +++ b/website/common/locales/en_GB/gear.json @@ -2079,5 +2079,9 @@ "weaponSpecialSpring2020WarriorNotes": "Fight or flight, this wing will serve you well! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.", "weaponSpecialSpring2020WarriorText": "Sharpened Wing", "weaponSpecialSpring2020RogueNotes": "You'll strike so fast it'll look even MORE blue! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.", - "weaponSpecialSpring2020RogueText": "Lazurite Blade" + "weaponSpecialSpring2020RogueText": "Lazurite Blade", + "headAccessoryMystery202004Notes": "They twitch just a bit if the scent of flowers drifts by--use them to find a pretty garden! Confers no benefit. April 2020 Subscriber Item.", + "headAccessoryMystery202004Text": "Mighty Monarch Antennae", + "backMystery202004Notes": "Make a quick flutter to the nearest flowery meadow or migrate across the continent with these beautiful wings! Confers no benefit. April 2020 Subscriber Item.", + "backMystery202004Text": "Mighty Monarch Wings" } diff --git a/website/common/locales/en_GB/subscriber.json b/website/common/locales/en_GB/subscriber.json index 4fa0c40d6e..b46a60f736 100644 --- a/website/common/locales/en_GB/subscriber.json +++ b/website/common/locales/en_GB/subscriber.json @@ -247,5 +247,6 @@ "monthlyMysteryItems": "Monthly Mystery Items", "subscribersReceiveBenefits": "Subscribers receive these useful benefits!", "giftASubscription": "Gift a Subscription", - "mysterySet202003": "Barbed Battler Set" + "mysterySet202003": "Barbed Battler Set", + "mysterySet202004": "Mighty Monarch Set" } diff --git a/website/common/locales/pt_BR/gear.json b/website/common/locales/pt_BR/gear.json index 07ea2b1039..c0cabd6c8f 100644 --- a/website/common/locales/pt_BR/gear.json +++ b/website/common/locales/pt_BR/gear.json @@ -2079,5 +2079,9 @@ "weaponSpecialSpring2020WarriorNotes": "Lutar ou voar, esta asa irá te atender bem! Aumenta a Força em <%= str %>. Equipamento de edição limitada da primavera de 2020.", "weaponSpecialSpring2020WarriorText": "Asa afiada", "weaponSpecialSpring2020RogueNotes": "Você atacará tão rápido que ficará ainda MAIS AZUL! Aumenta a Força em <%= str %>. Equipamento de edição limitada da primavera de 2020.", - "weaponSpecialSpring2020RogueText": "Lâmina de lazurita" + "weaponSpecialSpring2020RogueText": "Lâmina de lazurita", + "headAccessoryMystery202004Notes": "Elas tremem um pouco se o perfume das flores passa perto -- use-as para encontrar um belo jardim! Não confere benefícios. Item de assinante, Abril de 2020.", + "headAccessoryMystery202004Text": "Antenas do(a) Monarca poderoso(a)", + "backMystery202004Notes": "Faça um movimento rápido para o prado florido mais próximo ou migre pelo continente com essas lindas asas! Não confere benefícios. Item de assinante, Abril de 2020.", + "backMystery202004Text": "Asas do(a) Monarca poderoso(a)" } diff --git a/website/common/locales/pt_BR/subscriber.json b/website/common/locales/pt_BR/subscriber.json index 420cf38bb7..b55c97b988 100644 --- a/website/common/locales/pt_BR/subscriber.json +++ b/website/common/locales/pt_BR/subscriber.json @@ -247,5 +247,6 @@ "monthlyMysteryItems": "Itens misteriosos mensalmente", "subscribersReceiveBenefits": "Assinantes recebem esses benefícios úteis!", "giftASubscription": "Presentar uma Assinatura", - "mysterySet202003": "Conjunto do(a) gladiador(a) farpado(a)" + "mysterySet202003": "Conjunto do(a) gladiador(a) farpado(a)", + "mysterySet202004": "Conjunto do(a) Monarca poderoso(a)" } diff --git a/website/common/locales/zh/gear.json b/website/common/locales/zh/gear.json index 47198bb38d..b2731bf669 100644 --- a/website/common/locales/zh/gear.json +++ b/website/common/locales/zh/gear.json @@ -2079,5 +2079,7 @@ "armorSpecialSpring2020RogueText": "群青装甲", "armorSpecialSpring2020WarriorText": "外骨骼护甲", "armorSpecialSpring2020MageText": "旋风长袍", - "armorSpecialSpring2020HealerText": "防护的花瓣" + "armorSpecialSpring2020HealerText": "防护的花瓣", + "headAccessoryMystery202004Text": "强大的君主斑蝶触角", + "backMystery202004Text": "强大的君主斑蝶翅膀" } diff --git a/website/common/locales/zh/subscriber.json b/website/common/locales/zh/subscriber.json index 9807160ed9..1bd1802d5c 100644 --- a/website/common/locales/zh/subscriber.json +++ b/website/common/locales/zh/subscriber.json @@ -247,5 +247,6 @@ "monthlyMysteryItems": "每月神秘物品", "subscribersReceiveBenefits": "订阅者可以获得这些优越的好处!", "mysterySet202003": "倒刺斗士套装", - "giftASubscription": "赠送订阅" + "giftASubscription": "赠送订阅", + "mysterySet202004": "强大的君主斑蝶套装" } diff --git a/website/server/controllers/top-level/auth.js b/website/server/controllers/top-level/auth.js index ae8e08b87f..2d469960fe 100644 --- a/website/server/controllers/top-level/auth.js +++ b/website/server/controllers/top-level/auth.js @@ -28,7 +28,9 @@ api.logout = { async handler (req, res) { if (req.logout) req.logout(); // passportjs method req.session = null; - res.redirect('/'); + + const redirectUrl = req.query.redirectToLogin === 'true' ? '/login' : '/'; + res.redirect(redirectUrl); }, }; diff --git a/website/server/libs/email.js b/website/server/libs/email.js index c5192e82d1..27cf2b62b5 100644 --- a/website/server/libs/email.js +++ b/website/server/libs/email.js @@ -133,9 +133,9 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV return got.post(`${EMAIL_SERVER.url}/job`, { retry: 5, // retry the http request to the email server 5 times timeout: 60000, // wait up to 60s before timing out - auth: `${EMAIL_SERVER.auth.user}:${EMAIL_SERVER.auth.password}`, - json: true, - body: { + username: EMAIL_SERVER.auth.user, + password: EMAIL_SERVER.auth.password, + json: { type: 'email', data: { emailType, @@ -149,7 +149,7 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV backoff: { delay: 10 * 60 * 1000, type: 'fixed' }, }, }, - }).catch(err => logger.error(err)); + }).json().catch(err => logger.error(err, 'Error while sending an email.')); } return null; diff --git a/website/server/libs/i18n.js b/website/server/libs/i18n.js index 85f8c14880..7fd6069014 100644 --- a/website/server/libs/i18n.js +++ b/website/server/libs/i18n.js @@ -22,34 +22,10 @@ const momentLangsMapping = { }; export const approvedLanguages = [ - 'bg', - 'cs', - 'da', - 'de', - 'en', - 'en_GB', - 'en@pirate', - 'es', - 'es_419', - 'fr', - 'he', - 'hu', - 'id', - 'it', - 'ja', - 'nl', - 'pl', - 'pt', - 'pt_BR', - 'ro', - 'ru', - 'sk', - 'sr', - 'sv', - 'tr', - 'uk', - 'zh', - 'zh_TW', + 'bg', 'cs', 'da', 'de', 'en', 'en_GB', 'en@pirate', + 'es', 'es_419', 'fr', 'he', 'hu', 'id', 'it', + 'ja', 'nl', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk', + 'sr', 'sv', 'tr', 'uk', 'zh', 'zh_TW', ]; function _loadTranslations (locale) { diff --git a/website/server/libs/language.js b/website/server/libs/language.js new file mode 100644 index 0000000000..639698b25d --- /dev/null +++ b/website/server/libs/language.js @@ -0,0 +1,52 @@ +import accepts from 'accepts'; +import _ from 'lodash'; +import { + translations, + defaultLangCodes, + multipleVersionsLanguages, +} from './i18n'; + +function getUniqueListOfLanguages (languages) { + const acceptableLanguages = _(languages).map(lang => lang.slice(0, 2)).uniq().value(); + + const uniqueListOfLanguages = _.intersection(acceptableLanguages, defaultLangCodes); + + return uniqueListOfLanguages; +} + +function checkForApplicableLanguageVariant (originalLanguageOptions) { + const languageVariant = _.find(originalLanguageOptions, accepted => { + const trimmedAccepted = accepted.slice(0, 2); + + return multipleVersionsLanguages[trimmedAccepted]; + }); + + return languageVariant; +} + +export function getLanguageFromBrowser (req) { + const originalLanguageOptions = accepts(req).languages(); + const uniqueListOfLanguages = getUniqueListOfLanguages(originalLanguageOptions); + const baseLanguage = (uniqueListOfLanguages[0] || '').toLowerCase(); + const languageMapping = multipleVersionsLanguages[baseLanguage]; + + if (languageMapping) { + let languageVariant = checkForApplicableLanguageVariant(originalLanguageOptions); + + if (languageVariant) { + languageVariant = languageVariant.toLowerCase(); + } else { + return 'en'; + } + + return languageMapping[languageVariant] || baseLanguage; + } + return baseLanguage || 'en'; +} + +export function getLanguageFromUser (user, req) { + const preferredLang = user && user.preferences && user.preferences.language; + const lang = translations[preferredLang] ? preferredLang : getLanguageFromBrowser(req); + + return lang; +} diff --git a/website/server/libs/webhook.js b/website/server/libs/webhook.js index 81f2a5b482..738af5b71b 100644 --- a/website/server/libs/webhook.js +++ b/website/server/libs/webhook.js @@ -13,10 +13,10 @@ function sendWebhook (webhook, body, user) { const { url, lastFailureAt } = webhook; got.post(url, { - body, - json: true, + json: body, timeout: 30000, // wait up to 30s before timing out retry: 3, // retry the request up to 3 times + // Not calling .json() to parse the response because we simply ignore it }).catch(webhookErr => { // Log the error logger.error(webhookErr, 'Error while sending a webhook request.'); diff --git a/website/server/middlewares/auth.js b/website/server/middlewares/auth.js index b9f680db34..5527a0d0fb 100644 --- a/website/server/middlewares/auth.js +++ b/website/server/middlewares/auth.js @@ -7,6 +7,8 @@ import { model as User, } from '../models/user'; import gcpStackdriverTracer from '../libs/gcpTraceAgent'; +import common from '../../common'; +import { getLanguageFromUser } from '../libs/language'; const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'); const USER_FIELDS_ALWAYS_LOADED = ['_id', 'notifications', 'preferences', 'auth', 'flags']; @@ -72,7 +74,17 @@ export function authWithHeaders (options = {}) { .exec() .then(user => { if (!user) throw new NotAuthorized(res.t('invalidCredentials')); - if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', { communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id })); + + if (user.auth.blocked) { + // We want the accountSuspended message to be translated but the language + // middleware hasn't run yet so we pick it manually + const language = getLanguageFromUser(user, req); + + throw new NotAuthorized(common.i18n.t('accountSuspended', { + communityManagerEmail: COMMUNITY_MANAGER_EMAIL, + userId: user._id, + }, language)); + } res.locals.user = user; req.session.userId = user._id; diff --git a/website/server/middlewares/language.js b/website/server/middlewares/language.js index 1c49130464..bb9bbacb0e 100644 --- a/website/server/middlewares/language.js +++ b/website/server/middlewares/language.js @@ -1,60 +1,15 @@ -import accepts from 'accepts'; -import _ from 'lodash'; import { model as User } from '../models/user'; import common from '../../common'; import { translations, - defaultLangCodes, - multipleVersionsLanguages, } from '../libs/i18n'; +import { + getLanguageFromUser, + getLanguageFromBrowser, +} from '../libs/language'; const { i18n } = common; -function _getUniqueListOfLanguages (languages) { - const acceptableLanguages = _(languages).map(lang => lang.slice(0, 2)).uniq().value(); - - const uniqueListOfLanguages = _.intersection(acceptableLanguages, defaultLangCodes); - - return uniqueListOfLanguages; -} - -function _checkForApplicableLanguageVariant (originalLanguageOptions) { - const languageVariant = _.find(originalLanguageOptions, accepted => { - const trimmedAccepted = accepted.slice(0, 2); - - return multipleVersionsLanguages[trimmedAccepted]; - }); - - return languageVariant; -} - -function _getFromBrowser (req) { - const originalLanguageOptions = accepts(req).languages(); - const uniqueListOfLanguages = _getUniqueListOfLanguages(originalLanguageOptions); - const baseLanguage = (uniqueListOfLanguages[0] || '').toLowerCase(); - const languageMapping = multipleVersionsLanguages[baseLanguage]; - - if (languageMapping) { - let languageVariant = _checkForApplicableLanguageVariant(originalLanguageOptions); - - if (languageVariant) { - languageVariant = languageVariant.toLowerCase(); - } else { - return 'en'; - } - - return languageMapping[languageVariant] || baseLanguage; - } - return baseLanguage || 'en'; -} - -function _getFromUser (user, req) { - const preferredLang = user && user.preferences && user.preferences.language; - const lang = translations[preferredLang] ? preferredLang : _getFromBrowser(req); - - return lang; -} - export function attachTranslateFunction (req, res, next) { res.t = function reqTranslation (...args) { return i18n.t(...args, req.language); @@ -64,26 +19,33 @@ export function attachTranslateFunction (req, res, next) { } export function getUserLanguage (req, res, next) { - if (req.query.lang) { // In case the language is specified in the request url, use it + // In case the language is specified in the request url, use intersection + if (req.query.lang) { req.language = translations[req.query.lang] ? req.query.lang : 'en'; return next(); + } // If the request is authenticated, use the user's preferred language - } if (req.locals && req.locals.user) { - req.language = _getFromUser(req.locals.user, req); + if (res.locals && res.locals.user) { + req.language = getLanguageFromUser(res.locals.user, req); return next(); - } if (req.session && req.session.userId) { // Same thing if the user has a valid session + } + + // Same thing if the user has a valid session + if (req.session && req.session.userId) { return User.findOne({ _id: req.session.userId, }, 'preferences.language') .lean() .exec() .then(user => { - req.language = _getFromUser(user, req); + req.language = getLanguageFromUser(user, req); return next(); }) .catch(next); - } // Otherwise get from browser - req.language = _getFromUser(null, req); + } + + // Otherwise get from browser + req.language = getLanguageFromBrowser(req); return next(); } diff --git a/website/server/models/webhook.js b/website/server/models/webhook.js index 7fcd0a975b..f9a0f62dc7 100644 --- a/website/server/models/webhook.js +++ b/website/server/models/webhook.js @@ -58,6 +58,8 @@ export const schema = new Schema({ required: true, validate: [v => validator.isURL(v, { require_tld: !!IS_PRODUCTION, // eslint-disable-line camelcase + require_protocol: true, // TODO migrate existing ones + protocols: ['http', 'https'], }), shared.i18n.t('invalidUrl')], }, enabled: { $type: Boolean, required: true, default: true },