From 7e698694d3c0c6caea77437bf3331cb3f25749ad Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 12 Dec 2018 12:32:54 +0530 Subject: [PATCH 01/50] Added gzip compression for project listing endpoints as well after significant performance improvement in metadata list endpoint. Project list endpoint improvement would affect (positive way) managers and copilots --- package-lock.json | 902 ++++++-------------------------------------- src/routes/index.js | 2 + 2 files changed, 109 insertions(+), 795 deletions(-) diff --git a/package-lock.json b/package-lock.json index 228a50d6..e4181e06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,65 +18,6 @@ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" }, - "@types/body-parser": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", - "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", - "requires": { - "@types/connect": "3.4.32", - "@types/node": "10.9.4" - } - }, - "@types/connect": { - "version": "3.4.32", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", - "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", - "requires": { - "@types/node": "10.9.4" - } - }, - "@types/events": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" - }, - "@types/express": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", - "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", - "requires": { - "@types/body-parser": "1.17.0", - "@types/express-serve-static-core": "4.16.0", - "@types/serve-static": "1.13.2" - } - }, - "@types/express-jwt": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.34.tgz", - "integrity": "sha1-/b7kxq9cCiRu8qkz9VGZc8dxfwI=", - "requires": { - "@types/express": "4.16.0", - "@types/express-unless": "0.0.32" - } - }, - "@types/express-serve-static-core": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", - "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", - "requires": { - "@types/events": "1.2.0", - "@types/node": "10.9.4", - "@types/range-parser": "1.2.2" - } - }, - "@types/express-unless": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.0.32.tgz", - "integrity": "sha512-6YpJyFNlDDnPnRjMOvJCoDYlSDDmG/OEEUsPk7yhNkL4G9hUYtgab6vi1CcWsGSSSM0CsvNlWTG+ywAGnvF03g==", - "requires": { - "@types/express": "4.16.0" - } - }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", @@ -87,30 +28,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==" }, - "@types/mime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", - "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" - }, - "@types/node": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", - "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" - }, - "@types/range-parser": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", - "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" - }, - "@types/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", - "requires": { - "@types/express-serve-static-core": "4.16.0", - "@types/mime": "2.0.0" - } - }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -376,19 +293,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "2.1.2" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", @@ -421,89 +325,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "auth0-js": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.7.3.tgz", - "integrity": "sha512-iZAqoN4EbsNCS/3VkFPNb4glTyj8hq57T7gcUF+XH8Rua7hBTUzpb101K9zqcdUIBilIdF9XBLCTJ4JGgZ/oFA==", - "requires": { - "base64-js": "1.2.1", - "idtoken-verifier": "1.2.0", - "js-cookie": "2.2.0", - "qs": "6.5.1", - "superagent": "3.8.3", - "url-join": "1.1.0", - "winchan": "0.2.0" - }, - "dependencies": { - "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", - "requires": { - "ms": "2.1.1" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.2.5", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.2.1", - "methods": "1.1.2", - "mime": "1.4.1", - "qs": "6.5.1", - "readable-stream": "2.3.6" - } - } - } - }, "aws-sdk": { "version": "2.143.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.143.0.tgz", @@ -528,16 +349,6 @@ } } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, "axios": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", @@ -1596,14 +1407,6 @@ } } }, - "babel-runtime": { - "version": "6.6.1", - "resolved": "http://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", - "integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=", - "requires": { - "core-js": "2.5.1" - } - }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", @@ -1688,33 +1491,17 @@ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "dev": true }, - "backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", - "requires": { - "precond": "0.2.3" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base64-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, "beeper": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", @@ -1865,6 +1652,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -1928,6 +1716,7 @@ "version": "1.8.12", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "dev": true, "requires": { "dtrace-provider": "0.8.5", "moment": "2.22.2", @@ -1968,11 +1757,6 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -2129,21 +1913,6 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, - "codependency": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/codependency/-/codependency-0.1.4.tgz", - "integrity": "sha1-0XY6tyZL1wyR2WJumIYtN5K/jUo=", - "requires": { - "semver": "5.0.1" - }, - "dependencies": { - "semver": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.1.tgz", - "integrity": "sha1-n7P0AE+QDYPEeWj+QvdYPgWDLMk=" - } - } - }, "color-convert": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", @@ -2187,10 +1956,69 @@ "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz", "integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=" }, + "compressible": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", + "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==", + "requires": { + "mime-db": "1.37.0" + }, + "dependencies": { + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + } + } + }, + "compression": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", + "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", + "requires": { + "accepts": "1.3.5", + "bytes": "3.0.0", + "compressible": "2.0.15", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.2", + "vary": "1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.21", + "negotiator": "0.6.1" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "1.37.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "1.6.0", @@ -2322,7 +2150,8 @@ "core-js": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", - "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=", + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2375,11 +2204,6 @@ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" }, - "crypto-js": { - "version": "3.1.9-1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", - "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" - }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -2400,14 +2224,6 @@ "es5-ext": "0.10.35" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -2590,6 +2406,7 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.5.tgz", "integrity": "sha1-mOu6Ihr6xG4cOf02hY2Pk2dSS5I=", + "dev": true, "optional": true, "requires": { "nan": "2.7.0" @@ -2616,16 +2433,6 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" - } - }, "ecdsa-sig-formatter": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", @@ -3201,11 +3008,6 @@ "is-extglob": "1.0.0" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, "fancy-log": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", @@ -3216,16 +3018,6 @@ "time-stamp": "1.1.0" } }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -3962,23 +3754,23 @@ "dev": true, "optional": true }, - "string-width": { - "version": "1.0.2", + "string_decoder": { + "version": "1.1.1", "bundled": true, "dev": true, + "optional": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "safe-buffer": "5.1.1" } }, - "string_decoder": { - "version": "1.1.1", + "string-width": { + "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { - "safe-buffer": "5.1.1" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "strip-ansi": { @@ -4084,18 +3876,11 @@ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, "glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, "requires": { "inflight": "1.0.6", "inherits": "2.0.3", @@ -4484,33 +4269,6 @@ } } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - } - } - }, "has": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", @@ -4601,102 +4359,11 @@ "statuses": "1.4.0" } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" - } - }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, - "idtoken-verifier": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz", - "integrity": "sha512-8jmmFHwdPz8L73zGNAXHHOV9yXNC+Z0TUBN5rafpoaFaLFltlIFr1JkQa3FYAETP23eSsulVw0sBiwrE8jqbUg==", - "requires": { - "base64-js": "1.2.1", - "crypto-js": "3.1.9-1", - "jsbn": "0.1.1", - "superagent": "3.8.3", - "url-join": "1.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", - "requires": { - "ms": "2.1.1" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.2.5", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.2.1", - "methods": "1.1.2", - "mime": "1.4.1", - "qs": "6.5.1", - "readable-stream": "2.3.6" - } - } - } - }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", @@ -4735,6 +4402,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -5035,11 +4703,6 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, "is-unc-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", @@ -5094,11 +4757,6 @@ } } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, "istanbul": { "version": "1.0.0-alpha.2", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.0.0-alpha.2.tgz", @@ -5311,11 +4969,6 @@ "nopt": "3.0.6" } }, - "js-cookie": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz", - "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=" - }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -5337,27 +4990,12 @@ "esprima": "4.0.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", @@ -5367,11 +5005,6 @@ "jsonify": "0.0.0" } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, "json5": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", @@ -5421,17 +5054,6 @@ } } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, "jwa": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", @@ -5442,19 +5064,6 @@ "safe-buffer": "5.1.1" } }, - "jwks-rsa": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.3.0.tgz", - "integrity": "sha512-9q+d5VffK/FvFAjuXoddrq7zQybFSINV4mcwJJExGKXGyjWWpTt3vsn/aX33aB0heY02LK0qSyicdtRK0gVTig==", - "requires": { - "@types/express-jwt": "0.0.34", - "debug": "2.6.9", - "limiter": "1.1.3", - "lru-memoizer": "1.12.0", - "ms": "2.0.0", - "request": "2.88.0" - } - }, "jws": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", @@ -5504,31 +5113,6 @@ "invert-kv": "1.0.0" } }, - "le_node": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.7.1.tgz", - "integrity": "sha1-gxZAna2oK58pZXykBgZj+PEVUdE=", - "requires": { - "babel-runtime": "6.6.1", - "codependency": "0.1.4", - "json-stringify-safe": "5.0.1", - "lodash": "3.9.3", - "reconnect-core": "1.3.0", - "semver": "5.1.0" - }, - "dependencies": { - "lodash": { - "version": "3.9.3", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz", - "integrity": "sha1-AVnoaDL+/8bWHYUrEqlTuZSWvTI=" - }, - "semver": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", - "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" - } - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5586,11 +5170,6 @@ } } }, - "limiter": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", - "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==" - }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -5621,11 +5200,6 @@ } } }, - "lock": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/lock/-/lock-0.1.4.tgz", - "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0=" - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -5888,28 +5462,6 @@ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, - "lru-memoizer": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.12.0.tgz", - "integrity": "sha1-7+ZXBsyKnMZT+A8NWm6jitlQ41I=", - "requires": { - "lock": "0.1.4", - "lodash": "4.17.4", - "lru-cache": "4.0.2", - "very-fast-args": "1.1.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - } - } - }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -6029,11 +5581,6 @@ "regex-cache": "0.4.4" } }, - "millisecond": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/millisecond/-/millisecond-0.1.2.tgz", - "integrity": "sha1-bMWtOGJByrjniv+WT4cCjuyS2sU=" - }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -6062,6 +5609,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "1.1.8" } @@ -6069,12 +5617,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, "requires": { "minimist": "0.0.8" } @@ -6194,6 +5744,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "dev": true, "optional": true, "requires": { "mkdirp": "0.5.1", @@ -6222,6 +5773,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true, "optional": true }, "negotiator": { @@ -6330,11 +5882,6 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6404,10 +5951,16 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1.0.2" } @@ -6593,7 +6146,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-is-inside": { "version": "1.0.2", @@ -6651,11 +6205,6 @@ "through": "2.3.8" } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "pg": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/pg/-/pg-4.5.7.tgz", @@ -6789,11 +6338,6 @@ "xtend": "4.0.1" } }, - "precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -6875,12 +6419,8 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true }, "punycode": { "version": "1.3.2", @@ -7091,14 +6631,6 @@ "resolve": "1.5.0" } }, - "reconnect-core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.3.0.tgz", - "integrity": "sha1-+65SkZp4d9hE4yRtAaLyZwHIM8g=", - "requires": { - "backoff": "2.5.0" - } - }, "redefine": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/redefine/-/redefine-0.2.1.tgz", @@ -7240,81 +6772,6 @@ "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", "dev": true }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.8.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.1.0", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.20", - "oauth-sign": "0.9.0", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.4.3", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.20" - } - }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" - }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "requires": { - "mime-db": "1.36.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7395,6 +6852,7 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "dev": true, "requires": { "glob": "6.0.4" } @@ -7423,13 +6881,9 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", + "dev": true, "optional": true }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "samsam": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", @@ -7816,22 +7270,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "0.2.4", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" - } - }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -7849,7 +7287,13 @@ "stream-consume": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", - "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=" + "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "string-width": { "version": "1.0.2", @@ -7862,11 +7306,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -8075,80 +7514,6 @@ } } }, - "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", - "requires": { - "auth0-js": "9.7.3", - "axios": "0.12.0", - "bunyan": "1.8.12", - "config": "1.27.0", - "jsonwebtoken": "7.4.3", - "jwks-rsa": "1.3.0", - "le_node": "1.7.1", - "lodash": "4.17.4", - "millisecond": "0.1.2" - }, - "dependencies": { - "axios": { - "version": "0.12.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.12.0.tgz", - "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", - "requires": { - "follow-redirects": "0.0.7" - } - }, - "follow-redirects": { - "version": "0.0.7", - "resolved": "http://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", - "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", - "requires": { - "debug": "2.6.9", - "stream-consume": "0.1.0" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "isemail": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", - "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" - }, - "joi": { - "version": "6.10.1", - "resolved": "http://registry.npmjs.org/joi/-/joi-6.10.1.tgz", - "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", - "requires": { - "hoek": "2.16.3", - "isemail": "1.2.0", - "moment": "2.22.2", - "topo": "1.1.0" - } - }, - "jsonwebtoken": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", - "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", - "requires": { - "joi": "6.10.1", - "jws": "3.1.5", - "lodash.once": "4.1.1", - "ms": "2.0.0", - "xtend": "4.0.1" - } - }, - "topo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", - "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", - "requires": { - "hoek": "2.16.3" - } - } - } - }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -8303,22 +7668,6 @@ } } }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "1.1.29", - "punycode": "1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", @@ -8336,20 +7685,6 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -8519,11 +7854,6 @@ "querystring": "0.2.0" } }, - "url-join": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", - "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" - }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -8608,21 +7938,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "very-fast-args": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/very-fast-args/-/very-fast-args-1.1.0.tgz", - "integrity": "sha1-4W0dH6+KbllqJGQh/ZCneWPQs5Y=" - }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", @@ -8741,11 +8056,6 @@ "string-width": "1.0.2" } }, - "winchan": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.0.tgz", - "integrity": "sha1-OGMCjn+XSw2hQS8oQXukJJcqvZQ=" - }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -8777,7 +8087,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write": { "version": "0.2.1", @@ -8844,7 +8155,8 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true }, "yargs": { "version": "3.10.0", diff --git a/src/routes/index.js b/src/routes/index.js index 469c2c2f..c209a093 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -60,10 +60,12 @@ router.all( ); // Register all the routes +router.use('/v4/projects', compression()); router.route('/v4/projects') .post(require('./projects/create')) .get(require('./projects/list')); +router.use('/v4/projects/db', compression()); router.route('/v4/projects/db') .get(require('./projects/list-db')); From ff08ba45f602bffa1b2fe607cd1c9904c030ffd2 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 12 Dec 2018 12:34:02 +0530 Subject: [PATCH 02/50] temporary making feature branch deployable --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b315cea..3a1b3300 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ workflows: - test filters: branches: - only: ['dev', 'feature/auth0-proxy-server'] + only: ['dev', 'feature/gzip_compression_projects_list'] - deployProd: requires: - test From 86c3a8a346ceb896c6e9aa8ac0c2f11159f59671 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 12 Dec 2018 12:54:23 +0530 Subject: [PATCH 03/50] Removed the feature branch from deployable list --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3a1b3300..459ec723 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ workflows: - test filters: branches: - only: ['dev', 'feature/gzip_compression_projects_list'] + only: ['dev'] - deployProd: requires: - test From 0c1ce34a88f9d8a7378180e461406efff2e6bed0 Mon Sep 17 00:00:00 2001 From: Samir Gondzetovic Date: Sun, 16 Dec 2018 01:08:17 +0000 Subject: [PATCH 04/50] team management challenge and final fixes --- local/mock-services/server.js | 31 +- local/mock-services/services.json | 8 + ...20181201_create_project_member_invites.sql | 35 + package-lock.json | 1361 ++++++++++------- postman.json | 396 +++-- postman_environment.json | 122 +- src/constants.js | 16 + src/events/busApi.js | 25 + src/events/index.js | 8 +- src/events/projectMemberInvites/index.js | 76 + src/events/projectMembers/index.js | 10 +- src/models/milestone.js | 2 - src/models/project.js | 1 + src/models/projectMemberInvite.js | 86 ++ src/permissions/index.js | 4 + src/routes/index.js | 5 + src/routes/projectMemberInvites/create.js | 174 +++ .../projectMemberInvites/create.spec.js | 621 ++++++++ src/routes/projectMemberInvites/get.js | 35 + src/routes/projectMemberInvites/get.spec.js | 127 ++ src/routes/projectMemberInvites/update.js | 128 ++ .../projectMemberInvites/update.spec.js | 349 +++++ src/routes/projectMembers/create.js | 90 +- src/routes/projectMembers/create.spec.js | 439 +----- src/routes/projects/list-db.js | 12 +- src/routes/projects/list.js | 12 +- src/util.js | 43 +- swagger.yaml | 210 ++- 28 files changed, 3211 insertions(+), 1215 deletions(-) create mode 100644 migrations/20181201_create_project_member_invites.sql create mode 100644 src/events/projectMemberInvites/index.js create mode 100644 src/models/projectMemberInvite.js create mode 100644 src/routes/projectMemberInvites/create.js create mode 100644 src/routes/projectMemberInvites/create.spec.js create mode 100644 src/routes/projectMemberInvites/get.js create mode 100644 src/routes/projectMemberInvites/get.spec.js create mode 100644 src/routes/projectMemberInvites/update.js create mode 100644 src/routes/projectMemberInvites/update.spec.js diff --git a/local/mock-services/server.js b/local/mock-services/server.js index 029df5b7..550f276a 100644 --- a/local/mock-services/server.js +++ b/local/mock-services/server.js @@ -14,6 +14,7 @@ const middlewares = jsonServer.defaults(); const authMiddleware = require('./authMiddleware'); const members = require('./services.json').members; +const roles = require('./services.json').roles; server.use(middlewares); @@ -29,7 +30,12 @@ server.get('/v3/members/_search', (req, res) => { const ret = {}; const splitted = single.split(':'); // if the result can be parsed successfully - const parsed = jsprim.parseInteger(splitted[1], { allowTrailing: true, trimWhitespace: true }); + let parsed = Error(); + try { + parsed = jsprim.parseInteger(splitted[1], { allowTrailing: true, trimWhitespace: true }); + } catch (e) { + // no-empty + } if (parsed instanceof Error) { ret[splitted[0]] = splitted[1]; } else { @@ -60,6 +66,29 @@ server.get('/v3/members/_search', (req, res) => { res.status(200).json(response); }); +// add additional search route for project members +server.get('/roles', (req, res) => { + const filter = _.isString(req.query.filter) ? + req.query.filter.replace('%2520', ' ').replace('%20', ' ').split('=') : []; + const cloned = _.cloneDeep(roles); + const response = { + id: 'res1', + result: { + success: true, + status: 200, + }, + }; + const role = filter ? _.find(cloned, (single) => { + if (single.userId === filter[1]) { + return single.roles; + } + return null; + }) : null; + + response.result.content = role ? role.roles : []; + res.status(200).json(response); +}); + server.use(router); server.listen(PORT, () => { diff --git a/local/mock-services/services.json b/local/mock-services/services.json index d77800cb..e6a2b2f1 100644 --- a/local/mock-services/services.json +++ b/local/mock-services/services.json @@ -300,5 +300,13 @@ }, "version": "v3" } + ], + "roles": [ + { "userId": "40051334", "roles": [ { "roleName": "Connect Manager" } ] }, + { "userId": "40051332", "roles": [ { "roleName": "Connect Copilot" } ] }, + { "userId": "40051333", "roles": [ { "roleName": "administrator" } ] }, + { "userId": "40051336", "roles": [ { "roleName": "Connect Admin" }, { "roleName": "Connect Copilot" } ] }, + { "userId": "40051331", "roles": [ ] }, + { "userId": "40051335", "roles": [ ] } ] } diff --git a/migrations/20181201_create_project_member_invites.sql b/migrations/20181201_create_project_member_invites.sql new file mode 100644 index 00000000..a67df339 --- /dev/null +++ b/migrations/20181201_create_project_member_invites.sql @@ -0,0 +1,35 @@ +-- +-- CREATE NEW TABLES: +-- project_member_invites +-- + +-- +-- project_member_invites +-- + +CREATE TABLE project_member_invites ( + id bigint NOT NULL, + "projectId" bigint, + "userId" bigint, + email character varying(255), + role character varying(255) NOT NULL, + status character varying(255) NOT NULL, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedAt" timestamp with time zone, + "createdBy" integer NOT NULL, + "updatedBy" integer NOT NULL, + "deletedBy" bigint +); + +CREATE SEQUENCE project_member_invites_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER TABLE project_member_invites_id_seq OWNER BY project_member_invites.id; + +ALTER TABLE ONLY project_member_invites + ADD CONSTRAINT "project_member_invites_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES projects(id) ON UPDATE CASCADE ON DELETE CASCADE; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e4181e06..20e07864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,65 @@ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "3.4.32", + "@types/node": "10.12.15" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "10.12.15" + } + }, + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/express": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", + "requires": { + "@types/body-parser": "1.17.0", + "@types/express-serve-static-core": "4.16.0", + "@types/serve-static": "1.13.2" + } + }, + "@types/express-jwt": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.34.tgz", + "integrity": "sha1-/b7kxq9cCiRu8qkz9VGZc8dxfwI=", + "requires": { + "@types/express": "4.16.0", + "@types/express-unless": "0.0.32" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", + "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", + "requires": { + "@types/events": "1.2.0", + "@types/node": "10.12.15", + "@types/range-parser": "1.2.3" + } + }, + "@types/express-unless": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.0.32.tgz", + "integrity": "sha512-6YpJyFNlDDnPnRjMOvJCoDYlSDDmG/OEEUsPk7yhNkL4G9hUYtgab6vi1CcWsGSSSM0CsvNlWTG+ywAGnvF03g==", + "requires": { + "@types/express": "4.16.0" + } + }, "@types/geojson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", @@ -28,6 +87,30 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==" }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, + "@types/node": { + "version": "10.12.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz", + "integrity": "sha512-9kROxduaN98QghwwHmxXO2Xz3MaWf+I1sLVAA6KJDF5xix+IyXVhds0MAfdNwtcpSrzhaTsNB0/jnL86fgUhqA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "4.16.0", + "@types/mime": "2.0.0" + } + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -293,6 +376,19 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", @@ -325,6 +421,89 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "auth0-js": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.8.2.tgz", + "integrity": "sha512-fwUkIABBA0e1B6hfkePtjOFlhXzvOUc/ZFx3NE1X9Ij3VZeqtJK7QU/Pc6tar+NkOpgZbRUXkxEG5qPGiwixWQ==", + "requires": { + "base64-js": "1.2.1", + "idtoken-verifier": "1.2.0", + "js-cookie": "2.2.0", + "qs": "6.5.1", + "superagent": "3.8.3", + "url-join": "4.0.0", + "winchan": "0.2.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "2.1.1" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "requires": { + "component-emitter": "1.2.1", + "cookiejar": "2.1.1", + "debug": "3.2.6", + "extend": "3.0.1", + "form-data": "2.3.1", + "formidable": "1.2.1", + "methods": "1.1.2", + "mime": "1.4.1", + "qs": "6.5.1", + "readable-stream": "2.3.6" + } + } + } + }, "aws-sdk": { "version": "2.143.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.143.0.tgz", @@ -349,6 +528,16 @@ } } }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "axios": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", @@ -1407,6 +1596,14 @@ } } }, + "babel-runtime": { + "version": "6.6.1", + "resolved": "http://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", + "integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=", + "requires": { + "core-js": "2.5.1" + } + }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", @@ -1491,17 +1688,32 @@ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "dev": true }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2.3" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base64-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "0.14.5" + } + }, "beeper": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", @@ -1652,7 +1864,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -1716,7 +1927,6 @@ "version": "1.8.12", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", - "dev": true, "requires": { "dtrace-provider": "0.8.5", "moment": "2.22.2", @@ -1757,6 +1967,11 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -1820,7 +2035,6 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", - "fsevents": "1.2.4", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1913,6 +2127,21 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "codependency": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/codependency/-/codependency-0.1.4.tgz", + "integrity": "sha1-0XY6tyZL1wyR2WJumIYtN5K/jUo=", + "requires": { + "semver": "5.0.1" + }, + "dependencies": { + "semver": { + "version": "5.0.1", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.0.1.tgz", + "integrity": "sha1-n7P0AE+QDYPEeWj+QvdYPgWDLMk=" + } + } + }, "color-convert": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", @@ -2017,8 +2246,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.0", @@ -2150,8 +2378,7 @@ "core-js": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", - "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=", - "dev": true + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" }, "core-util-is": { "version": "1.0.2", @@ -2204,6 +2431,11 @@ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -2224,6 +2456,14 @@ "es5-ext": "0.10.35" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -2406,7 +2646,6 @@ "version": "0.8.5", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.5.tgz", "integrity": "sha1-mOu6Ihr6xG4cOf02hY2Pk2dSS5I=", - "dev": true, "optional": true, "requires": { "nan": "2.7.0" @@ -2433,6 +2672,15 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" + } + }, "ecdsa-sig-formatter": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", @@ -3008,6 +3256,11 @@ "is-extglob": "1.0.0" } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "fancy-log": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", @@ -3018,6 +3271,16 @@ "time-stamp": "1.1.0" } }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -3285,549 +3548,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz", "integrity": "sha1-jNF0XItPiinIyuw5JHaSG6GV9WA=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.0", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "function-bind": { "version": "1.1.1", @@ -3876,11 +3603,18 @@ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, "glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, "requires": { "inflight": "1.0.6", "inherits": "2.0.3", @@ -4269,6 +4003,33 @@ } } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "6.6.1", + "har-schema": "2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + } + } + }, "has": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", @@ -4341,12 +4102,9 @@ "dev": true }, "http-aws-es": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/http-aws-es/-/http-aws-es-1.1.3.tgz", - "integrity": "sha1-ZJUYQ7XFETBQclcNfCxQn3gUTWs=", - "requires": { - "aws-sdk": "2.143.0" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/http-aws-es/-/http-aws-es-4.0.0.tgz", + "integrity": "sha512-5OJVj9/JSNOVFgIOnBK+9fwDePd35PF1odskYjp/aqstuurZy1XdmHoDP+wPE5LH9Pe/TasIJyARyH7aJnLh/A==" }, "http-errors": { "version": "1.6.2", @@ -4359,11 +4117,107 @@ "statuses": "1.4.0" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.15.2" + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, + "idtoken-verifier": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz", + "integrity": "sha512-8jmmFHwdPz8L73zGNAXHHOV9yXNC+Z0TUBN5rafpoaFaLFltlIFr1JkQa3FYAETP23eSsulVw0sBiwrE8jqbUg==", + "requires": { + "base64-js": "1.2.1", + "crypto-js": "3.1.9-1", + "jsbn": "0.1.1", + "superagent": "3.8.3", + "url-join": "1.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "2.1.1" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "requires": { + "component-emitter": "1.2.1", + "cookiejar": "2.1.1", + "debug": "3.2.6", + "extend": "3.0.1", + "form-data": "2.3.1", + "formidable": "1.2.1", + "methods": "1.1.2", + "mime": "1.4.1", + "qs": "6.5.1", + "readable-stream": "2.3.6" + } + }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + } + } + }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", @@ -4402,7 +4256,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -4703,6 +4556,11 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "is-unc-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", @@ -4757,6 +4615,11 @@ } } }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "istanbul": { "version": "1.0.0-alpha.2", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.0.0-alpha.2.tgz", @@ -4969,6 +4832,11 @@ "nopt": "3.0.6" } }, + "js-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz", + "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=" + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -4990,12 +4858,27 @@ "esprima": "4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", @@ -5005,6 +4888,11 @@ "jsonify": "0.0.0" } }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "json5": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", @@ -5054,6 +4942,17 @@ } } }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "jwa": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", @@ -5064,6 +4963,19 @@ "safe-buffer": "5.1.1" } }, + "jwks-rsa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.3.0.tgz", + "integrity": "sha512-9q+d5VffK/FvFAjuXoddrq7zQybFSINV4mcwJJExGKXGyjWWpTt3vsn/aX33aB0heY02LK0qSyicdtRK0gVTig==", + "requires": { + "@types/express-jwt": "0.0.34", + "debug": "2.6.9", + "limiter": "1.1.3", + "lru-memoizer": "1.12.0", + "ms": "2.0.0", + "request": "2.88.0" + } + }, "jws": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", @@ -5113,6 +5025,31 @@ "invert-kv": "1.0.0" } }, + "le_node": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", + "integrity": "sha512-NXzjxBskZ4QawTNwlGdRG05jYU0LhV2nxxmP3x7sRMHyROV0jPdyyikO9at+uYrWX3VFt0Y/am11oKITedx0iw==", + "requires": { + "babel-runtime": "6.6.1", + "codependency": "0.1.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.11", + "reconnect-core": "1.3.0", + "semver": "5.1.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "semver": { + "version": "5.1.0", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + } + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5170,6 +5107,11 @@ } } }, + "limiter": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", + "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==" + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -5200,6 +5142,11 @@ } } }, + "lock": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/lock/-/lock-0.1.4.tgz", + "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0=" + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -5462,6 +5409,28 @@ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, + "lru-memoizer": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.12.0.tgz", + "integrity": "sha1-7+ZXBsyKnMZT+A8NWm6jitlQ41I=", + "requires": { + "lock": "0.1.4", + "lodash": "4.17.4", + "lru-cache": "4.0.2", + "very-fast-args": "1.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + } + } + }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -5581,6 +5550,11 @@ "regex-cache": "0.4.4" } }, + "millisecond": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/millisecond/-/millisecond-0.1.2.tgz", + "integrity": "sha1-bMWtOGJByrjniv+WT4cCjuyS2sU=" + }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -5609,7 +5583,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "1.1.8" } @@ -5617,14 +5590,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -5744,7 +5715,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "dev": true, "optional": true, "requires": { "mkdirp": "0.5.1", @@ -5773,7 +5743,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "dev": true, "optional": true }, "negotiator": { @@ -5882,6 +5851,11 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5960,7 +5934,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1.0.2" } @@ -6146,8 +6119,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -6205,6 +6177,11 @@ "through": "2.3.8" } }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "pg": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/pg/-/pg-4.5.7.tgz", @@ -6338,6 +6315,11 @@ "xtend": "4.0.1" } }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -6419,8 +6401,12 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" }, "punycode": { "version": "1.3.2", @@ -6631,6 +6617,14 @@ "resolve": "1.5.0" } }, + "reconnect-core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.3.0.tgz", + "integrity": "sha1-+65SkZp4d9hE4yRtAaLyZwHIM8g=", + "requires": { + "backoff": "2.5.0" + } + }, "redefine": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/redefine/-/redefine-0.2.1.tgz", @@ -6772,6 +6766,81 @@ "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", "dev": true }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.8.0", + "caseless": "0.12.0", + "combined-stream": "1.0.7", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.3", + "har-validator": "5.1.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.21", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.7", + "mime-types": "2.1.21" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "1.37.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6852,7 +6921,6 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "dev": true, "requires": { "glob": "6.0.4" } @@ -6881,9 +6949,13 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz", "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", - "dev": true, "optional": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "samsam": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", @@ -7270,6 +7342,22 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "0.2.4", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.2", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.2", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "safer-buffer": "2.1.2", + "tweetnacl": "0.14.5" + } + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -7287,13 +7375,7 @@ "stream-consume": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", - "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=" }, "string-width": { "version": "1.0.2", @@ -7306,6 +7388,11 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -7514,6 +7601,43 @@ } } }, + "tc-core-library-js": { + "version": "github:appirio-tech/tc-core-library-js#02350d46d3b8d89ee4686d5c1a5d0086943cbfe8", + "requires": { + "auth0-js": "9.8.2", + "axios": "0.12.0", + "bunyan": "1.8.12", + "jsonwebtoken": "8.3.0", + "jwks-rsa": "1.3.0", + "le_node": "1.8.0", + "lodash": "4.17.11", + "millisecond": "0.1.2" + }, + "dependencies": { + "axios": { + "version": "0.12.0", + "resolved": "http://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", + "requires": { + "follow-redirects": "0.0.7" + } + }, + "follow-redirects": { + "version": "0.0.7", + "resolved": "http://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", + "requires": { + "debug": "2.6.9", + "stream-consume": "0.1.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + } + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -7668,6 +7792,22 @@ } } }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "1.1.31", + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", @@ -7685,6 +7825,19 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -7845,6 +7998,21 @@ } } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -7854,6 +8022,11 @@ "querystring": "0.2.0" } }, + "url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=" + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -7938,6 +8111,21 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, + "very-fast-args": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/very-fast-args/-/very-fast-args-1.1.0.tgz", + "integrity": "sha1-4W0dH6+KbllqJGQh/ZCneWPQs5Y=" + }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", @@ -8056,6 +8244,11 @@ "string-width": "1.0.2" } }, + "winchan": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.1.tgz", + "integrity": "sha512-QrG9q+ObfmZBxScv0HSCqFm/owcgyR5Sgpiy1NlCZPpFXhbsmNHhTiLWoogItdBUi0fnU7Io/5ABEqRta5/6Dw==" + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -8087,8 +8280,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", @@ -8155,8 +8347,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { "version": "3.10.0", diff --git a/postman.json b/postman.json index 8f6bdcc6..ecd5c408 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "160fcac7-f74a-4047-a4e4-b53f08d991c5", + "_postman_id": "e810fc27-5518-4cc5-8f90-6b1423c6b0b4", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -120,7 +120,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/2", @@ -439,13 +439,13 @@ "name": "Project Members", "item": [ { - "name": "Create project member with no payload", + "name": "Create project manager with valid values", "request": { "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token}}" + "value": "Bearer {{jwt-token-manager-40051334}}" }, { "key": "Content-Type", @@ -468,14 +468,14 @@ "members" ] }, - "description": "Request payload is mandatory while creating project. If no request payload is specified this should result in 422 status code." + "description": "If the request payload is valid, than project member should be created." }, "response": [] }, { - "name": "Create project copilot with invalid userId", + "name": "Update project member", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -488,10 +488,10 @@ ], "body": { "mode": "raw", - "raw": "{\n\"param\":{\n\t\"role\": \"copilot\"\n}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"isPrimary\": true\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members", + "raw": "{{api-url}}/v4/projects/1/members/1", "host": [ "{{api-url}}" ], @@ -499,17 +499,18 @@ "v4", "projects", "1", - "members" + "members", + "1" ] }, - "description": "Certain fields are mandatory while creating project. If invalid fields are specified this should result in 422 status code." + "description": "Update a project's member." }, "response": [] }, { - "name": "Create project copilot with valid values", + "name": "Update project member with isPrimary False", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -522,28 +523,29 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"isPrimary\": false\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7/members", + "raw": "{{api-url}}/v4/projects/1/members/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", - "members" + "1", + "members", + "1" ] }, - "description": "If the request payload is valid, than project member should be created." + "description": "Update a project's member." }, "response": [] }, { - "name": "Create project member, if user already registered", + "name": "wrong role", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -556,28 +558,28 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" + "raw": " {\n \"param\": {\n \"role\": \"wrong\"\n }\n } " }, "url": { - "raw": "{{api-url}}/v4/projects/1/members", + "raw": "{{api-url}}/v4/projects/3/members/5", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "members" + "3", + "members", + "5" ] - }, - "description": "If the request payload is valid and user is already registered with the specified role than this should result in 400." + } }, "response": [] }, { - "name": "Create project manager with valid values", + "name": "Delete project member", "request": { - "method": "POST", + "method": "DELETE", "header": [ { "key": "Authorization", @@ -590,28 +592,29 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"manager\",\n\t\t\"userId\": 40051330,\n\t\t\"isPrimary\": true\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/7/members", + "raw": "{{api-url}}/v4/projects/1/members/40051331", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", - "members" + "1", + "members", + "40051331" ] }, - "description": "If the request payload is valid, than project manager should be added. This should sync with the direct project is project is associated with direct project." + "description": "Delete a project's member" }, "response": [] }, { - "name": "Create project customer with valid values", + "name": "editing project member roles & primary option", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -624,28 +627,69 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"customer\",\n\t\t\"userId\": 40051332,\n\t\t\"isPrimary\": true\n\t}\n}" + "raw": " {\n \"param\": {\n \"role\": \"manager\",\n \"isPrimary\": true\n }\n } " }, "url": { - "raw": "{{api-url}}/v4/projects/7/members", + "raw": "{{api-url}}/v4/projects/1/members/2", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", - "members" + "1", + "members", + "2" ] + } + }, + "response": [] + } + ] + }, + { + "name": "Project Member Invites", + "item": [ + { + "name": "Invite valid userIds", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"customer\"\n\t}\n}" }, - "description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project." + "url": { + "raw": "{{api-url}}/v4/projects/1/members/invite", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "members", + "invite" + ] + } }, "response": [] }, { - "name": "Update project member", + "name": "Invite valid emails", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -653,15 +697,17 @@ }, { "key": "Content-Type", - "value": "application/json" + "name": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"isPrimary\": true\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"customer\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/1", + "raw": "{{api-url}}/v4/projects/1/members/invite", "host": [ "{{api-url}}" ], @@ -670,17 +716,52 @@ "projects", "1", "members", - "1" + "invite" ] + } + }, + "response": [] + }, + { + "name": "Invite email with manager role", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}" }, - "description": "Update a project's member." + "url": { + "raw": "{{api-url}}/v4/projects/1/members/invite", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "members", + "invite" + ] + } }, "response": [] }, { - "name": "Update project member with isPrimary False", + "name": "Invite manager and target has no MANAGER_ROLES", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -688,15 +769,17 @@ }, { "key": "Content-Type", + "name": "Content-Type", + "type": "text", "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"isPrimary\": false\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"manager\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/1", + "raw": "{{api-url}}/v4/projects/1/members/invite", "host": [ "{{api-url}}" ], @@ -705,17 +788,52 @@ "projects", "1", "members", - "1" + "invite" ] + } + }, + "response": [] + }, + { + "name": "Invite manager and requester has no MANAGER_ROLES", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-member2-40051335}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"manager\"\n\t}\n}" }, - "description": "Update a project's member." + "url": { + "raw": "{{api-url}}/v4/projects/1/members/invite", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "members", + "invite" + ] + } }, "response": [] }, { - "name": "wrong role", + "name": "Invite with both userIds and emails", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -723,33 +841,35 @@ }, { "key": "Content-Type", + "name": "Content-Type", + "type": "text", "value": "application/json" } ], "body": { "mode": "raw", - "raw": " {\n \"param\": {\n \"role\": \"wrong\"\n }\n } " + "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/3/members/5", + "raw": "{{api-url}}/v4/projects/1/members/invite", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "3", + "1", "members", - "5" + "invite" ] } }, "response": [] }, { - "name": "Delete project member", + "name": "Update invite status with userId", "request": { - "method": "DELETE", + "method": "PUT", "header": [ { "key": "Authorization", @@ -757,34 +877,71 @@ }, { "key": "Content-Type", - "value": "application/json" + "name": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"userId\": \"40051331\",\n\t\t\"status\": \"accepted\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/3/members/5", + "raw": "{{api-url}}/v4/projects/1/members/invite", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "3", + "1", "members", - "5" + "invite" ] + } + }, + "response": [] + }, + { + "name": "Update invite status with email", + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"email\": \"hello@world.com\",\n\t\t\"status\": \"canceled\"\n\t}\n}" }, - "description": "Delete a project's member" + "url": { + "raw": "{{api-url}}/v4/projects/1/members/invite", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "members", + "invite" + ] + } }, "response": [] }, { - "name": "editing project member roles & primary option", + "name": "Update invite with both userId and email", "request": { - "method": "PATCH", + "method": "PUT", "header": [ { "key": "Authorization", @@ -792,15 +949,17 @@ }, { "key": "Content-Type", - "value": "application/json" + "name": "Content-Type", + "value": "application/json", + "type": "text" } ], "body": { "mode": "raw", - "raw": " {\n \"param\": {\n \"role\": \"manager\",\n \"isPrimary\": true\n }\n } " + "raw": "{\n\t\"param\": {\n\t\t\"userId\": \"40051331\",\n\t\t\"email\": \"hello@world.com\",\n\t\t\"status\": \"accepted\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/2", + "raw": "{{api-url}}/v4/projects/1/members/invite", "host": [ "{{api-url}}" ], @@ -809,7 +968,46 @@ "projects", "1", "members", - "2" + "invite" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve current user invite", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-member-40051331}}" + }, + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/1/members/invite", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "1", + "members", + "invite" ] } }, @@ -963,14 +1161,14 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/7", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7" + "1" ] }, "description": "Get a project by id. project members and attachments should also be returned." @@ -1783,7 +1981,7 @@ ], "body": { "mode": "raw", - "raw": " {\n \"param\": {\n \"role\": \"copilot\",\n \"isPrimary\": true\n }\n } " + "raw": "" }, "url": { "raw": "https://api.topcoder-dev.com/v3/direct/projects", @@ -2141,7 +2339,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20\n\t}\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases", @@ -2174,7 +2372,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20\n\t}\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases?fields=status,name,budget", @@ -2213,7 +2411,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20\n\t}\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases?sort=status desc", @@ -2252,7 +2450,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20\n\t}\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases?sort=order desc", @@ -2291,7 +2489,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20\n\t}\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/1/phases/1", @@ -2636,7 +2834,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", @@ -2669,7 +2867,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1", @@ -2809,7 +3007,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/productTemplates", @@ -2842,7 +3040,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/productTemplates/3", @@ -2949,7 +3147,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"key\": \"generic\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTypes", @@ -2982,7 +3180,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTypes", @@ -3015,7 +3213,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTypes/generic", @@ -3155,7 +3353,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/productCategories", @@ -3188,7 +3386,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", @@ -3558,7 +3756,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines?filter=reference%3Dphase%26referenceId%3D1", @@ -3595,7 +3793,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/1", @@ -3826,7 +4024,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/1/milestones", @@ -3859,7 +4057,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/1/milestones?sort=order desc", @@ -3898,7 +4096,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/1/milestones/1", @@ -4410,7 +4608,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/metadata/milestoneTemplates", @@ -4443,7 +4641,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/metadata/milestoneTemplates?filter=reference%3DproductTemplate%26referenceId%3D1", @@ -4482,7 +4680,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/metadata/milestoneTemplates?filter=reference%3DproductTemplate%26referenceId%3D1&sort=order desc", @@ -4525,7 +4723,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/timelines/metadata/milestoneTemplates/1", diff --git a/postman_environment.json b/postman_environment.json index 261834ae..3192362a 100644 --- a/postman_environment.json +++ b/postman_environment.json @@ -1,63 +1,63 @@ { - "id": "53925cd5-ff42-43a2-bb87-29f9aa73ffd9", - "name": "tc-project-service", - "values": [ - { - "key": "api-url", - "value": "http://localhost:3000", - "description": "", - "enabled": true - }, - { - "key": "jwt-token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-admin-40051333", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-member-40051331", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzEiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.pDtRzcGQjgCBD6aLsW-1OFhzmrv5mXhb8YLDWbGAnKo", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-copilot-40051332", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBDb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjo0MDA1MTMzMiwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImlhdCI6MTQ3MDYyMDA0NH0.DnX17gBaVF2JTuRai-C2BDSdEjij9da_s4eYcMIjP0c", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-manager-40051334", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-member2-40051335", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4", - "description": "", - "enabled": true - }, - { - "key": "jwt-token-connectAdmin-40051336", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30", - "description": "", - "enabled": true - }, - { - "key": "inactive-userId", - "value": "1800075", - "description": "", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2018-08-28T10:28:37.251Z", - "_postman_exported_using": "Postman/6.2.5" + "id": "be71a5b6-f6f0-413c-99ae-56f21f10dd53", + "name": "tc-project-service", + "values": [ + { + "key": "api-url", + "value": "http://localhost:8001", + "description": "", + "enabled": true + }, + { + "key": "jwt-token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-admin-40051333", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-member-40051331", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzEiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.pDtRzcGQjgCBD6aLsW-1OFhzmrv5mXhb8YLDWbGAnKo", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-copilot-40051332", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBDb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjo0MDA1MTMzMiwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImlhdCI6MTQ3MDYyMDA0NH0.DnX17gBaVF2JTuRai-C2BDSdEjij9da_s4eYcMIjP0c", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-manager-40051334", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-member2-40051335", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4", + "description": "", + "enabled": true + }, + { + "key": "jwt-token-connectAdmin-40051336", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30", + "description": "", + "enabled": true + }, + { + "key": "inactive-userId", + "value": "1800075", + "description": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-12-04T21:50:56.610Z", + "_postman_exported_using": "Postman/6.5.2" } \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index bc99c3b2..47d8c9ce 100644 --- a/src/constants.js +++ b/src/constants.js @@ -15,10 +15,13 @@ export const MILESTONE_STATUS = PROJECT_STATUS; export const PROJECT_MEMBER_ROLE = { MANAGER: 'manager', + OBSERVER: 'observer', CUSTOMER: 'customer', COPILOT: 'copilot', }; +export const PROJECT_MEMBER_MANAGER_ROLES = [PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.OBSERVER]; + export const USER_ROLE = { TOPCODER_ADMIN: 'administrator', MANAGER: 'Connect Manager', @@ -59,6 +62,9 @@ export const EVENT = { MILESTONE_ADDED: 'milestone.added', MILESTONE_UPDATED: 'milestone.updated', MILESTONE_REMOVED: 'milestone.removed', + + PROJECT_MEMBER_INVITE_CREATED: 'project.member.invite.created', + PROJECT_MEMBER_INVITE_UPDATED: 'project.member.invite.updated', }, }; @@ -121,6 +127,10 @@ export const BUS_API_EVENT = { TOPIC_UPDATED: 'notifications.connect.project.topic.updated', POST_CREATED: 'notifications.connect.project.post.created', POST_UPDATED: 'notifications.connect.project.post.edited', + + // Project Member Invites + PROJECT_MEMBER_INVITE_CREATED: 'notifications.connect.project.member.invite.created', + PROJECT_MEMBER_INVITE_UPDATED: 'notifications.connect.project.member.invite.updated', }; export const REGEX = { @@ -141,3 +151,9 @@ export const MILESTONE_TEMPLATE_REFERENCES = { PRODUCT_TEMPLATE: 'productTemplate', }; +export const INVITE_STATUS = { + PENDING: 'pending', + ACCEPTED: 'accepted', + REFUSED: 'refused', + CANCELED: 'canceled', +}; diff --git a/src/events/busApi.js b/src/events/busApi.js index 50ff0f61..953a0431 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -658,4 +658,29 @@ module.exports = (app, logger) => { }).catch(err => null); // eslint-disable-line no-unused-vars } }); + + app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, ({ req, userId, email }) => { + logger.debug('receive PROJECT_MEMBER_INVITE_CREATED event'); + const projectId = _.parseInt(req.params.projectId); + + // send event to bus api + createEvent(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, { + projectId, + userId, + email, + }, logger); + }); + + app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED, ({ req, userId, email, status }) => { + logger.debug('receive PROJECT_MEMBER_INVITE_UPDATED event'); + const projectId = _.parseInt(req.params.projectId); + + // send event to bus api + createEvent(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED, { + projectId, + userId, + email, + status, + }, logger); + }); }; diff --git a/src/events/index.js b/src/events/index.js index 23a3f037..806b5b1c 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -4,7 +4,9 @@ import { projectCreatedHandler, projectUpdatedHandler, projectDeletedHandler, projectUpdatedKafkaHandler } from './projects'; import { projectMemberAddedHandler, projectMemberRemovedHandler, projectMemberUpdatedHandler } from './projectMembers'; -import { projectAttachmentAddedHandler, projectAttachmentRemovedHandler, +import { projectMemberInviteCreatedHandler, + projectMemberInviteUpdatedHandler } from './projectMemberInvites'; +import { projectAttachmentRemovedHandler, projectAttachmentUpdatedHandler } from './projectAttachments'; import { projectPhaseAddedHandler, projectPhaseRemovedHandler, projectPhaseUpdatedHandler } from './projectPhases'; @@ -31,7 +33,9 @@ export const rabbitHandlers = { [EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED]: projectMemberAddedHandler, [EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED]: projectMemberRemovedHandler, [EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED]: projectMemberUpdatedHandler, - [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED]: projectAttachmentAddedHandler, + [EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED]: projectMemberInviteCreatedHandler, + [EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED]: projectMemberInviteUpdatedHandler, + [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED]: projectMemberInviteUpdatedHandler, [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED]: projectAttachmentRemovedHandler, [EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED]: projectAttachmentUpdatedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED]: projectPhaseAddedHandler, diff --git a/src/events/projectMemberInvites/index.js b/src/events/projectMemberInvites/index.js new file mode 100644 index 00000000..f67d07ed --- /dev/null +++ b/src/events/projectMemberInvites/index.js @@ -0,0 +1,76 @@ +/** + * Event handlers for project member invite create and update + */ +import _ from 'lodash'; +import Promise from 'bluebird'; +import { updateESPromise } from '../projectMembers'; + +/** + * Project member invite careted event handler + * @param {Object} logger logger + * @param {Object} msg event payload + * @param {Object} channel channel to ack / nack + * @return {undefined} + */ +const projectMemberInviteCreatedHandler = Promise.coroutine(function* a(logger, msg, channel) { + try { + const origRequestId = msg.properties.correlationId; + const newInvite = JSON.parse(msg.content.toString()); + const projectId = newInvite.projectId; + + // handle ES Update + // add new invite to document invites array + const updateDocPromise = Promise.coroutine(function* (doc) { // eslint-disable-line + // now merge the updated changes and reindex the document + const invites = _.isArray(doc._source.invites) ? doc._source.invites : []; // eslint-disable-line no-underscore-dangle + invites.push(newInvite); + return _.merge(doc._source, { invites }); // eslint-disable-line no-underscore-dangle + }); + + yield updateESPromise(logger, origRequestId, projectId, updateDocPromise); + logger.debug('elasticsearch index updated successfully'); + channel.ack(msg); + } catch (error) { + logger.error('Error handling projectMemberInviteCreated Event', error); + // if the message has been redelivered dont attempt to reprocess it + channel.nack(msg, false, !msg.fields.redelivered); + } +}); + +/** + * Project member invite updated event handler + * @param {Object} logger logger + * @param {Object} msg event payload + * @param {Object} channel channel to ack / nack + * @return {undefined} + */ +const projectMemberInviteUpdatedHandler = Promise.coroutine(function* a(logger, msg, channel) { + try { + const origRequestId = msg.properties.correlationId; + const updatedInvite = JSON.parse(msg.content.toString()); + const projectId = updatedInvite.projectId; + + // handle ES Update + // remove invite in document invites array, based on either userId or email + const updateDocPromise = Promise.coroutine(function* (doc) { // eslint-disable-line + // now merge the updated changes and reindex the document + const invites = _.isArray(doc._source.invites) ? doc._source.invites : []; // eslint-disable-line no-underscore-dangle + _.remove(invites, invite => (!!updatedInvite.email && invite.email === updatedInvite.email) || + (!!updatedInvite.userId && invite.userId === updatedInvite.userId)); + return _.merge(doc._source, { invites }); // eslint-disable-line no-underscore-dangle + }); + + yield updateESPromise(logger, origRequestId, projectId, updateDocPromise); + logger.debug('elasticsearch index updated successfully'); + channel.ack(msg); + } catch (error) { + logger.error('Error handling projectMemberInviteCreated Event', error); + // if the message has been redelivered dont attempt to reprocess it + channel.nack(msg, false, !msg.fields.redelivered); + } +}); + +module.exports = { + projectMemberInviteCreatedHandler, + projectMemberInviteUpdatedHandler, +}; diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index a0a0ff84..82510b7b 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -24,7 +24,7 @@ const updateESPromise = Promise.coroutine(function* a(logger, requestId, project id: projectId, body: { doc: updatedDoc }, }) - .then(() => logger.debug('elasticsearch project document updated, member updated successfully')); + .then(() => logger.debug('elasticsearch project document updated successfully')); } catch (error) { logger.error('Error caught updating ES document', error); return Promise.reject(error); @@ -83,10 +83,13 @@ const projectMemberAddedHandler = Promise.coroutine(function* a(logger, msg, cha const updateDocPromise = Promise.coroutine(function* (doc) { // eslint-disable-line func-names const memberDetails = yield util.getMemberDetailsByUserIds([newMember.userId], logger, origRequestId); const payload = _.merge(newMember, _.pick(memberDetails[0], 'handle', 'firstName', 'lastName', 'email')); - // now merge the updated changes and reindex the document + // now merge the updated changes and reindex the document for members const members = _.isArray(doc._source.members) ? doc._source.members : []; // eslint-disable-line no-underscore-dangle members.push(payload); - return _.merge(doc._source, { members }); // eslint-disable-line no-underscore-dangle + // now merge the updated changes and reindex the document for invites + const invites = _.isArray(doc._source.invites) ? doc._source.invites : []; // eslint-disable-line no-underscore-dangle + _.remove(invites, invite => invite === payload.email || invite === payload.userId); + return _.merge(doc._source, { members, invites }); // eslint-disable-line no-underscore-dangle }); yield Promise.all([directUpdatePromise(), updateESPromise(logger, origRequestId, projectId, updateDocPromise)]); logger.debug('elasticsearch index updated successfully and co-pilot/manager updated in direct project'); @@ -205,4 +208,5 @@ module.exports = { projectMemberAddedHandler, projectMemberRemovedHandler, projectMemberUpdatedHandler, + updateESPromise, }; diff --git a/src/models/milestone.js b/src/models/milestone.js index 53429883..76246a52 100644 --- a/src/models/milestone.js +++ b/src/models/milestone.js @@ -42,7 +42,6 @@ module.exports = (sequelize, DataTypes) => { * @param timelineId the id of timeline */ getTimelineDuration(timelineId) { - console.log('getTimelineDuration'); const where = { timelineId, hidden: false }; return this.findAll({ where, @@ -75,7 +74,6 @@ module.exports = (sequelize, DataTypes) => { scheduledDuration += m.duration; } }); - console.log(`${completedDuration} completed out of ${scheduledDuration} duration`); if (scheduledDuration > 0) { progress = Math.round((completedDuration / scheduledDuration) * 100); } diff --git a/src/models/project.js b/src/models/project.js index 7fdf81d0..5339b5b4 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -98,6 +98,7 @@ module.exports = function defineProject(sequelize, DataTypes) { Project.hasMany(models.ProjectMember, { as: 'members', foreignKey: 'projectId' }); Project.hasMany(models.ProjectAttachment, { as: 'attachments', foreignKey: 'projectId' }); Project.hasMany(models.ProjectPhase, { as: 'phases', foreignKey: 'projectId' }); + Project.hasMany(models.ProjectMemberInvite, { as: 'memberInvites', foreignKey: 'projectId' }); }, /** diff --git a/src/models/projectMemberInvite.js b/src/models/projectMemberInvite.js new file mode 100644 index 00000000..864217bb --- /dev/null +++ b/src/models/projectMemberInvite.js @@ -0,0 +1,86 @@ + +import _ from 'lodash'; +import { PROJECT_MEMBER_ROLE, INVITE_STATUS } from '../constants'; + +module.exports = function defineProjectMemberInvite(sequelize, DataTypes) { + const ProjectMemberInvite = sequelize.define('ProjectMemberInvite', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + projectId: DataTypes.BIGINT, + userId: DataTypes.BIGINT, + email: { + type: DataTypes.STRING, + validate: { + isEmail: true, + }, + }, + role: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [_.values(PROJECT_MEMBER_ROLE)], + }, + }, + status: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [_.values(INVITE_STATUS)], + }, + }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + deletedBy: DataTypes.BIGINT, + }, { + tableName: 'project_member_invites', + paranoid: true, + timestamps: true, + createdAt: 'createdAt', + updatedAt: 'updatedAt', + deletedAt: 'deletedAt', + indexes: [ + { fields: ['projectId'] }, + { fields: ['status'] }, + { fields: ['deletedAt'] }, + ], + classMethods: { + getPendingInvitesForProject(projectId) { + return this.findAll({ + where: { + projectId, + status: INVITE_STATUS.PENDING, + }, + raw: true, + }); + }, + getPendingInviteByEmailOrUserId(projectId, email, userId) { + const where = { status: INVITE_STATUS.PENDING }; + + if (email) { + _.assign(where, { email }); + } else if (userId) { + _.assign(where, { userId }); + } + return this.findOne({ + where, + }); + }, + getProjectInvitesForUser(email, userId) { + const where = { status: INVITE_STATUS.PENDING }; + + if (email) { + _.assign(where, { email }); + } else if (userId) { + _.assign(where, { userId }); + } + return this.findAll({ + where, + }).then(res => _.without(_.map(res, 'projectId'), null)); + }, + }, + }); + + return ProjectMemberInvite; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 2bdfd10e..0b9880e3 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -70,4 +70,8 @@ module.exports = () => { Authorizer.setPolicy('milestone.view', projectView); Authorizer.setPolicy('metadata.list', true); // anyone can view all metadata + + Authorizer.setPolicy('projectMemberInvite.create', projectView); + Authorizer.setPolicy('projectMemberInvite.put', true); + Authorizer.setPolicy('projectMemberInvite.get', true); }; diff --git a/src/routes/index.js b/src/routes/index.js index c209a093..dabe7454 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -177,6 +177,11 @@ router.route('/v4/timelines/metadata/milestoneTemplates/:milestoneTemplateId(\\d .patch(require('./milestoneTemplates/update')) .delete(require('./milestoneTemplates/delete')); +router.route('/v4/projects/:projectId(\\d+)/members/invite') + .post(require('./projectMemberInvites/create')) + .put(require('./projectMemberInvites/update')) + .get(require('./projectMemberInvites/get')); + // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js new file mode 100644 index 00000000..d56fc2f7 --- /dev/null +++ b/src/routes/projectMemberInvites/create.js @@ -0,0 +1,174 @@ + + +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES, + MANAGER_ROLES, INVITE_STATUS, EVENT } from '../../constants'; + +/** + * API to create member invite to project. + * + */ +const permissions = tcMiddleware.permissions; + +const addMemberValidations = { + body: { + param: Joi.object().keys({ + userIds: Joi.array().items(Joi.number()).optional().min(1), + emails: Joi.array().items(Joi.string().email()).optional().min(1), + role: Joi.any().valid(_.values(PROJECT_MEMBER_ROLE)).required(), + }).required(), + }, +}; + +module.exports = [ + // handles request validations + validate(addMemberValidations), + permissions('projectMemberInvite.create'), + (req, res, next) => { + const invite = req.body.param; + + if (!invite.userIds && !invite.emails) { + const err = new Error('Either userIds or emails are required'); + err.status = 400; + return next(err); + } + + if (!util.hasRoles(req, MANAGER_ROLES) && invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { + const err = new Error(`You are not allowed to invite user as ${invite.role}`); + err.status = 403; + return next(err); + } + + const members = req.context.currentProjectMembers; + const projectId = _.parseInt(req.params.projectId); + + const promises = []; + if (invite.userIds) { + // permission: + // user has to have constants.MANAGER_ROLES role + // to be invited as PROJECT_MEMBER_ROLE.MANAGER + if (invite.role === PROJECT_MEMBER_ROLE.MANAGER) { + _.forEach(invite.userIds, (userId) => { + req.log.info(userId); + promises.push(util.getUserRoles(userId, req.log, req.id)); + }); + } + + // validate each userId is not already a member + const alreadyMembers = []; + _.forEach(members, (member) => { + if (invite.userIds.includes(member.userId)) { + alreadyMembers.push(member.userId); + } + }); + if (alreadyMembers.length > 0) { + const err = new Error(`${alreadyMembers.join()} are already members of project ${projectId}`); + err.status = 400; + return next(err); + } + } + + if (invite.emails) { + // email invites can only be used for CUSTOMER role + if (invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { // eslint-disable-line no-lonely-if + const err = new Error(`Emails can only be used for ${PROJECT_MEMBER_ROLE.CUSTOMER}`); + err.status = 400; + return next(err); + } + } + + if (promises.length === 0) { + promises.push(Promise.resolve()); + } + return Promise.all(promises).then((rolesList) => { + if (!!invite.userIds && _.includes(PROJECT_MEMBER_MANAGER_ROLES, invite.role)) { + req.log.debug('Chekcing if userId is allowed as manager'); + const forbidUserList = []; + _.zip(invite.userIds, rolesList).forEach((data) => { + const [userId, roles] = data; + + if (!util.hasIntersection(MANAGER_ROLES, roles)) { + forbidUserList.push(userId); + } + }); + if (forbidUserList.length > 0) { + const err = new Error(`${forbidUserList.join()} cannot be added with a Manager role to the project`); + err.status = 403; + return next(err); + } + } + return models.ProjectMemberInvite.getPendingInvitesForProject(projectId) + .then((invites) => { + req.log.debug('Chekcing if user has been invited'); + // validate for each userId/email there is no existing invitation + const alreadyInvites = []; + _.forEach(invites, (i) => { + if (invite.userIds) { + if (invite.userIds.includes(i.userId)) { + alreadyInvites.push(i.userId); + } + } else if (invite.emails.includes(i.email)) { + alreadyInvites.push(i.email); + } + }); + if (alreadyInvites.length > 0) { + const err = new Error(`${alreadyInvites.join()} are already invited`); + err.status = 400; + return next(err); + } + + const data = { + projectId, + role: invite.role, + status: INVITE_STATUS.PENDING, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }; + const invitePromises = []; + if (invite.userIds) { + invite.userIds.forEach((userId) => { + const dataNew = _.clone(data); + _.assign(dataNew, { + userId, + }); + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + data.userId = null; + + if (invite.emails) { + invite.emails.forEach((email) => { + const dataNew = _.clone(data); + _.assign(dataNew, { + email, + }); + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + + req.log.debug('Creating invites'); + return models.sequelize.Promise.all(invitePromises) + .then((values) => { + values.forEach((v) => { + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, { + req, + userId: v.userId, + email: v.email, + }); + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, + v, + { correlationId: req.id }, + ); + }); + return res.status(201).json(util.wrapResponse(req.id, values, null, 201)); + }); + }); + }).catch(err => next(err)); + }, +]; diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js new file mode 100644 index 00000000..90473c4c --- /dev/null +++ b/src/routes/projectMemberInvites/create.spec.js @@ -0,0 +1,621 @@ +/* eslint-disable no-unused-expressions */ +import _ from 'lodash'; +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; + +import models from '../../models'; +import util from '../../util'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { USER_ROLE, PROJECT_MEMBER_ROLE, INVITE_STATUS, BUS_API_EVENT } from '../../constants'; + +const should = chai.should(); + +describe('Project Member Invite create', () => { + let project1; + let project2; + let invite1; + beforeEach((done) => { + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + }).then(() => + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test2', + description: 'test project2', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p2) => { + project2 = p2; + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051335, + email: null, + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((in1) => { + invite1 = in1.get({ + plain: true, + }); + done(); + }); + })); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('POST /projects/{id}/members/invite', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return 201 if userIds and emails are presented the same time', + (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + userIds: [40051332], + emails: ['hello@world.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + res.body.result.status.should.equal(400); + done(); + } + }); + }); + + it('should return 400 if neither userIds or email is presented', + (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + res.body.result.status.should.equal(400); + done(); + } + }); + }); + + it('should return 403 if try to create copilot without MANAGER_ROLES', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'copilot', + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*You are not allowed to invite user as/); + done(); + } + }); + }); + + it('should return 400 if user has a pending invite', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [invite1.userId], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + res.body.result.status.should.equal(400); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*already invited/); + done(); + } + }); + }); + + it('should return 403 if try to create copilot with MEMBER', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.CUSTOMER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'copilot', + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*You are not allowed to invite user as/); + done(); + } + }); + }); + + it('should return 201 and add new email invite as customer', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['hello@world.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content[0]; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.projectId.should.equal(project2.id); + resJson.email.should.equal('hello@world.com'); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; + done(); + } + }); + }); + + it('should return 201 and add new user invite as customer', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content[0]; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.projectId.should.equal(project2.id); + resJson.userId.should.equal(40152855); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; + done(); + } + }); + }); + + it('should return 403 if try to create manager without MANAGER_ROLES', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*not allowed to invite user as/); + done(); + } + }); + }); + + it('should return 201 if try to create manager with MANAGER_ROLES', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content[0]; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(40152855); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; + done(); + } + }); + }); + it('should return 400 if already in the project', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userIds: [40051332], + role: 'manager', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(400); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*are already members of project/); + done(); + } + }); + }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED message when userId invite added', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userIds: [3], + role: PROJECT_MEMBER_ROLE.CUSTOMER, + }, + }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({ + projectId: project1.id, + userId: 3, + email: null, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED message when email invite added', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + emails: ['hello@world.com'], + role: PROJECT_MEMBER_ROLE.CUSTOMER, + }, + }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({ + projectId: project1.id, + userId: null, + email: 'hello@world.com', + })).should.be.true; + done(); + }); + } + }); + }); + }); + }); +}); diff --git a/src/routes/projectMemberInvites/get.js b/src/routes/projectMemberInvites/get.js new file mode 100644 index 00000000..84ce2366 --- /dev/null +++ b/src/routes/projectMemberInvites/get.js @@ -0,0 +1,35 @@ + + +import _ from 'lodash'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +/** + * API to update invite member to project. + * + */ +const permissions = tcMiddleware.permissions; + +module.exports = [ + // handles request validations + permissions('projectMemberInvite.get'), + (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const currentUserId = req.authUser.userId; + let invite; + return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, null, currentUserId) + .then((_invite) => { + invite = _invite; + if (!invite) { + // check there is an existing invite for the user with status PENDING + // handle 404 + const err = new Error('invite not found for project id ' + + `${projectId}, userId ${currentUserId}`); + err.status = 404; + return next(err); + } + return res.json(util.wrapResponse(req.id, invite)); + }); + }, +]; diff --git a/src/routes/projectMemberInvites/get.spec.js b/src/routes/projectMemberInvites/get.spec.js new file mode 100644 index 00000000..b5a1757d --- /dev/null +++ b/src/routes/projectMemberInvites/get.spec.js @@ -0,0 +1,127 @@ +/* eslint-disable no-unused-expressions */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import { INVITE_STATUS } from '../../constants'; + +const should = chai.should(); + +describe('GET Project', () => { + let project1; + let project2; + before((done) => { + testUtil.clearDb() + .then(() => { + const p1 = models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + const pm1 = models.ProjectMember.create({ + userId: 40051333, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + // create invite + const invite1 = models.ProjectMemberInvite.create({ + userId: 40051331, + email: null, + projectId: project1.id, + role: 'customer', + createdBy: 1, + updatedBy: 1, + status: INVITE_STATUS.PENDING, + }); + return Promise.all([pm1, invite1]); + }); + + const p2 = models.Project.create({ + type: 'visual_design', + billingAccountId: 1, + name: 'test2', + description: 'test project2', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project2 = p; + }); + return Promise.all([p1, p2]) + .then(() => done()); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('GET /projects/{id}/members/invite', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get(`/v4/projects/${project2.id}/members/invite`) + .expect(403, done); + }); + + it('should return 404 if requested project doesn\'t exist', (done) => { + request(server) + .get('/v4/projects/14343323/members/invite') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return the invite if user is invited to this project', (done) => { + request(server) + .get(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + should.exist(resJson.projectId); + resJson.userId.should.be.eql(40051331); + resJson.status.should.be.eql(INVITE_STATUS.PENDING); + done(); + } + }); + }); + + it('should return 404 if user is not invited to this project', (done) => { + request(server) + .get(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(404) + .end(() => { + done(); + }); + }); + }); +}); diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js new file mode 100644 index 00000000..47da01cd --- /dev/null +++ b/src/routes/projectMemberInvites/update.js @@ -0,0 +1,128 @@ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, USER_ROLE, INVITE_STATUS, EVENT } from '../../constants'; + +/** + * API to update invite member to project. + * + */ +const permissions = tcMiddleware.permissions; + +const updateMemberValidations = { + body: { + param: Joi.object() + .keys({ + userId: Joi.number().optional(), + email: Joi.string() + .email() + .optional(), + status: Joi.any() + .valid(_.values(INVITE_STATUS)) + .required(), + }) + .required(), + }, +}; + +module.exports = [ + // handles request validations + validate(updateMemberValidations), + permissions('projectMemberInvite.put'), + (req, res, next) => { + const putInvite = req.body.param; + const projectId = _.parseInt(req.params.projectId); + + // not userId and email at the same time + if (!!putInvite.userId && !!putInvite.email) { + const err = new Error('userId and email cannot be presented in the same request'); + err.status = 400; + return next(err); + } + + // userId or email should be provided + if (!putInvite.userId && !putInvite.email) { + const err = new Error('userId or email should be provided'); + err.status = 400; + return next(err); + } + + let invite; + return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId( + projectId, + putInvite.email, + putInvite.userId, + ).then((_invite) => { + invite = _invite; + if (!invite) { + // check there is an existing invite for the user with status PENDING + // handle 404 + const err = new Error( + `invite not found for project id ${projectId}, email ${putInvite.email} and userId ${putInvite.userId}`, + ); + err.status = 404; + return next(err); + } + + req.log.debug('Chekcing user permission for updating invite'); + let error = null; + if (putInvite.status === INVITE_STATUS.CANCELED) { + if (util.hasRole(req, USER_ROLE.COPILOT) && invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { + error = `Copilot can cancel invites only for ${PROJECT_MEMBER_ROLE.CUSTOMER}`; + } else if (!util.hasRoles(req, MANAGER_ROLES)) { + error = `Project members can cancel invites only for ${PROJECT_MEMBER_ROLE.CUSTOMER}`; + } + } else if (!!putInvite.userId && putInvite.userId !== req.authUser.userId) { + error = 'Project members can only update invites for themselves'; + } + + if (error) { + const err = new Error(error); + err.status = 403; + return next(err); + } + + req.log.debug('Updating invite status'); + return invite + .update({ + status: putInvite.status, + }) + .then((updatedInvite) => { + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED, { + req, + userId: updatedInvite.userId, + email: updatedInvite.email, + status: updatedInvite.status, + }); + req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_UPDATED, updatedInvite, { + correlationId: req.id, + }); + + req.log.debug('Adding user to project'); + // add user to project if accept invite + if (updatedInvite.status === INVITE_STATUS.ACCEPTED) { + return models.ProjectMember.getActiveProjectMembers(projectId) + .then((members) => { + req.context = req.context || {}; + req.context.currentProjectMembers = members; + const member = { + projectId, + role: updatedInvite.role, + userId: updatedInvite.userId, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }; + return util + .addUserToProject(req, member) + .then(() => res.json(util.wrapResponse(req.id, updatedInvite))) + .catch(err => next(err)); + }); + } + return res.json(util.wrapResponse(req.id, updatedInvite)); + }); + }); + }, +]; diff --git a/src/routes/projectMemberInvites/update.spec.js b/src/routes/projectMemberInvites/update.spec.js new file mode 100644 index 00000000..9dab290b --- /dev/null +++ b/src/routes/projectMemberInvites/update.spec.js @@ -0,0 +1,349 @@ +/* eslint-disable no-unused-expressions */ +import _ from 'lodash'; +import request from 'supertest'; +import sinon from 'sinon'; +import chai from 'chai'; +import models from '../../models'; +import server from '../../app'; +import util from '../../util'; +import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT, USER_ROLE, PROJECT_MEMBER_ROLE, INVITE_STATUS } from '../../constants'; + +const should = chai.should(); + +describe('Project member invite update', () => { + let project1; + let invite1; + let invite2; + + beforeEach((done) => { + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + models.ProjectMember.create({ + userId: 40051334, + projectId: project1.id, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then(() => { + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051331, + email: null, + role: PROJECT_MEMBER_ROLE.CUSTOMER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((in1) => { + invite1 = in1.get({ + plain: true, + }); + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051332, + email: null, + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((in2) => { + invite2 = in2.get({ + plain: true, + }); + done(); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('PUT /projects/{id}/members/invite', () => { + const body = { + param: { + status: 'accepted', + }, + }; + + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return 403 if user does not have permissions', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/invite`) + .send(body) + .expect(403, done); + }); + + it('should return 404 if user has no invite', (done) => { + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 123, + status: INVITE_STATUS.CANCELED, + }, + }) + .expect('Content-Type', /json/) + .expect(404) + .end(() => { + done(); + }); + }); + + it('should return 400 no userId or email is presented', (done) => { + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + status: INVITE_STATUS.CANCELED, + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(400); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*userId or email should be provided/); + done(); + } + }); + }); + + it('should return 400 if userId and email are presented the same time', (done) => { + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + userId: 40051332, + email: 'hello@world.com', + status: INVITE_STATUS.CANCELED, + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(400); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*userId and email cannot be presented in the same request/); + done(); + } + }); + }); + + it('should return 403 if try to update MANAGER role invite with copilot', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: invite2.userId, + status: INVITE_STATUS.CANCELED, + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*Copilot can cancel invites only for/); + done(); + } + }); + }); + + it('should return 403 if try to update others invite with CUSTOMER', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.CUSTOMER, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .send({ + param: { + userId: invite1.userId, + status: INVITE_STATUS.CANCELED, + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*Project members can cancel invites only for customer/); + done(); + } + }); + }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('Accept invite sends BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED ' + + 'and BUS_API_EVENT.PROJECT_MEMBER_ADDED messages', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ + param: { + userId: invite1.userId, + status: INVITE_STATUS.ACCEPTED, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledThrice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED, sinon.match({ + projectId: project1.id, + userId: invite1.userId, + status: INVITE_STATUS.ACCEPTED, + email: null, + })).should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.MEMBER_JOINED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + userId: invite1.userId, + initiatorUserId: 40051331, + })).should.be.true; + createEventSpy.thirdCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + userId: invite1.userId, + initiatorUserId: 40051331, + })).should.be.true; + done(); + }); + } + }); + }); + }); + }); +}); diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index ac809e0a..96c34a85 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -1,88 +1,72 @@ -import validate from 'express-validation'; import _ from 'lodash'; -import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; -import models from '../../models'; import util from '../../util'; -import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, EVENT } from '../../constants'; +import { USER_ROLE, PROJECT_MEMBER_ROLE, MANAGER_ROLES, INVITE_STATUS } from '../../constants'; +import models from '../../models'; /** * API to add a project member. - * + * add members directly (only managers and copilots) + * user being added is current user */ const permissions = tcMiddleware.permissions; -const addMemberValidations = { - body: { - param: Joi.object().keys({ - userId: Joi.number().required(), - isPrimary: Joi.boolean(), - role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, - PROJECT_MEMBER_ROLE.COPILOT).required(), - }).required(), - }, -}; - module.exports = [ // handles request validations - validate(addMemberValidations), permissions('project.addMember'), (req, res, next) => { - const member = req.body.param; + let targetRole; + if (util.hasRoles(req, [USER_ROLE.MANAGER])) { + targetRole = PROJECT_MEMBER_ROLE.MANAGER; + } else if (util.hasRoles(req, [USER_ROLE.COPILOT])) { + targetRole = PROJECT_MEMBER_ROLE.COPILOT; + } else { + const err = new Error('Only copilot or manager is able to call this endpoint'); + err.status = 401; + return next(err); + } + const projectId = _.parseInt(req.params.projectId); - // set defaults - _.assign(member, { + const member = { projectId, + role: targetRole, + userId: req.authUser.userId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, - }); - const members = req.context.currentProjectMembers; + }; - // check if member is already registered - const existingMember = _.find(members, m => m.userId === member.userId); - if (existingMember) { - const err = new Error(`User already registered for role: ${existingMember.role}`); - err.status = 400; - return next(err); - } - // check if another member is registered for this role as primary, - // if not mark this member as primary - if (_.isUndefined(member.isPrimary)) { - member.isPrimary = _.isUndefined(_.find(members, m => m.isPrimary && m.role === member.role)); - } let promise = Promise.resolve(); if (member.role === PROJECT_MEMBER_ROLE.MANAGER) { promise = util.getUserRoles(member.userId, req.log, req.id); } + req.log.debug('creating member', member); - let newMember = null; - // register member return promise.then((memberRoles) => { + req.log.debug(memberRoles); if (member.role === PROJECT_MEMBER_ROLE.MANAGER && (!memberRoles || !util.hasIntersection(MANAGER_ROLES, memberRoles))) { const err = new Error('This user can\'t be added as a Manager to the project'); err.status = 400; return next(err); } - return models.ProjectMember.create(member) - .then((_newMember) => { - newMember = _newMember.get({ plain: true }); - // publish event - req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, - newMember, - { correlationId: req.id }, - ); - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, { req, member: newMember }); - res.status(201).json(util.wrapResponse(req.id, newMember, 1, 201)); - }) - .catch((err) => { - req.log.error('Unable to register ', err); - next(err); - }); - }); + + return util.addUserToProject(req, member) + .then((newMember) => { + let invite; + return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, null, newMember.userId) + .then((_invite) => { + invite = _invite; + if (!invite) { + return res.status(201).json(util.wrapResponse(req.id, newMember, 1, 201)); + } + return invite.update({ + status: INVITE_STATUS.ACCEPTED, + }).then(() => res.status(201).json(util.wrapResponse(req.id, newMember, 1, 201))); + }); + }); + }).catch(err => next(err)); }, ]; diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index 429b50de..f55ad90a 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -9,13 +9,12 @@ import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; import busApi from '../../services/busApi'; -import { USER_ROLE, PROJECT_MEMBER_ROLE, BUS_API_EVENT } from '../../constants'; +import { USER_ROLE, BUS_API_EVENT } from '../../constants'; const should = chai.should(); describe('Project Members create', () => { let project1; - let project2; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -25,7 +24,7 @@ describe('Project Members create', () => { billingAccountId: 1, name: 'test1', description: 'test project1', - status: 'draft', + status: 'reviewed', details: {}, createdBy: 1, updatedBy: 1, @@ -33,31 +32,8 @@ describe('Project Members create', () => { lastActivityUserId: '1', }).then((p) => { project1 = p; - // create members - models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }); - }).then(() => - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test2', - description: 'test project2', - status: 'reviewed', - details: {}, - createdBy: 1, - updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }).then((p2) => { - project2 = p2; - done(); - })); + done(); + }); }); }); @@ -80,224 +56,11 @@ describe('Project Members create', () => { .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ - param: { - userId: 1, - role: 'customer', - }, - }) .expect('Content-Type', /json/) .expect(403, done); }); - it('should return 400 if user is already registered', (done) => { - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ - param: { - userId: 40051332, - role: 'customer', - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - res.body.result.status.should.equal(400); - done(); - } - }); - }); - - it('should return 201 and register copilot member for project', (done) => { - request(server) - .post(`/v4/projects/${project2.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - userId: 1, - role: 'copilot', - }, - }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('copilot'); - resJson.isPrimary.should.be.truthy; - resJson.projectId.should.equal(project2.id); - resJson.userId.should.equal(1); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - done(); - } - }); - }); - - it('should return 201 and register customer member', (done) => { - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - userId: 1, - role: 'customer', - }, - }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('customer'); - resJson.isPrimary.should.be.truthy; - resJson.projectId.should.equal(project1.id); - resJson.userId.should.equal(1); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - done(); - } - }); - }); - - /* - // TODO this test is no logner valid since updating direct is async - // we should convert this test to async msg handler test - it.skip('should return 500 if error to add copilot', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.reject(new Error('error message')) - }) - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) - request(server) - .post('/v4/projects/' + project1.id + '/members/') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot - }) - .send({ param: {userId: 2, role: 'copilot'}}) - .expect('Content-Type', /json/) - .expect(500) - .end(function(err, res) { - if (err) { - return done(err) - } - const result = res.body.result - result.success.should.be.false - result.status.should.equal(500) - result.content.message.should.equal('error message') - done() - }) - }) - */ - - it('should return 201 and register copilot member', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { - copilotProjectId: 2, - }, - }, - }, - }), - }); - const postSpy = sinon.spy(mockHttpClient, 'post'); - // var amqPubSpy = sinon.spy(server.services.pubsub, 'publish') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - userId: 3, - role: 'copilot', - }, - }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('copilot'); - resJson.isPrimary.should.be.truthy; - resJson.projectId.should.equal(project1.id); - resJson.userId.should.equal(3); - postSpy.should.have.been.calledOnce; - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - done(); - } - }); - }); - - it('should return 400 for trying to add customers as manager', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: [{ - roleName: 'Topcoder User', - }], - }, - }, - }), - }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - userId: 3, - role: 'manager', - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - const errorMessage = _.get(resJson, 'message', ''); - sinon.assert.match(errorMessage, /.*can't be added as a Manager/); - done(); - } - }); - }); - - it('should return 400 for trying to add copilot as manager', (done) => { + it('should return 201 and then 400 if user is already registered', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ status: 200, @@ -316,77 +79,43 @@ describe('Project Members create', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - userId: 3, - role: 'manager', - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - done(); - } - }); - }); + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('copilot'); + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - it('should return 201 and register Connect Manager as manager', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: [{ - roleName: USER_ROLE.MANAGER, - }], - }, - }, - }), + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err2, res2) => { + if (err2) { + done(err); + } else { + res2.body.result.status.should.equal(400); + done(); + } + }); + } }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - userId: 3, - role: 'manager', - }, - }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('manager'); - resJson.isPrimary.should.be.truthy; - resJson.projectId.should.equal(project1.id); - resJson.userId.should.equal(3); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - done(); - } - }); }); - it('should return 201 and register Connect Admin as manager', (done) => { + it('should return 201 and register customer member', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ status: 200, @@ -397,45 +126,12 @@ describe('Project Members create', () => { success: true, status: 200, content: [{ - roleName: USER_ROLE.CONNECT_ADMIN, + roleName: USER_ROLE.MANAGER, }], }, }, }), - }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - userId: 3, - role: 'manager', - }, - }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('manager'); - resJson.isPrimary.should.be.truthy; - resJson.projectId.should.equal(project1.id); - resJson.userId.should.equal(3); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; - done(); - } - }); - }); - - it('should return 201 and register Topcoder Admin as manager', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ + post: () => Promise.resolve({ status: 200, data: { id: 'requesterId', @@ -443,9 +139,7 @@ describe('Project Members create', () => { result: { success: true, status: 200, - content: [{ - roleName: USER_ROLE.TOPCODER_ADMIN, - }], + content: {}, }, }, }), @@ -456,12 +150,6 @@ describe('Project Members create', () => { .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) - .send({ - param: { - userId: 3, - role: 'manager', - }, - }) .expect('Content-Type', /json/) .expect(201) .end((err, res) => { @@ -473,7 +161,7 @@ describe('Project Members create', () => { resJson.role.should.equal('manager'); resJson.isPrimary.should.be.truthy; resJson.projectId.should.equal(project1.id); - resJson.userId.should.equal(3); + resJson.userId.should.equal(40051334); server.services.pubsub.publish.calledWith('project.member.added').should.be.true; done(); } @@ -527,12 +215,6 @@ describe('Project Members create', () => { .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) - .send({ - param: { - userId: 3, - role: PROJECT_MEMBER_ROLE.MANAGER, - }, - }) .expect(201) .end((err) => { if (err) { @@ -561,10 +243,6 @@ describe('Project Members create', () => { Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ - param: { - userId: 3, - role: PROJECT_MEMBER_ROLE.COPILOT, - }, }) .expect(201) .end((err) => { @@ -586,39 +264,6 @@ describe('Project Members create', () => { } }); }); - - it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when customer added', (done) => { - request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - userId: 3, - role: PROJECT_MEMBER_ROLE.CUSTOMER, - }, - }) - .expect(201) - .end((err) => { - if (err) { - done(err); - } else { - testUtil.wait(() => { - createEventSpy.calledTwice.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED); - createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ - projectId: project1.id, - projectName: project1.name, - projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, - userId: 40051332, - initiatorUserId: 40051332, - })).should.be.true; - done(); - }); - } - }); - }); }); }); }); diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 68b07761..a8ed05d2 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -140,14 +140,22 @@ module.exports = [ models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { + let allowedProjectIds = accessibleProjectIds; + // get projects with pending invite for current user + const invites = models.ProjectMemberInvite.getProjectInvitesForUser( + req.authUser.email, + req.authUser.userId); + if (invites) { + allowedProjectIds = _.union(allowedProjectIds, invites); + } // filter based on accessible if (_.get(criteria.filters, 'id', null)) { criteria.filters.id.$in = _.intersection( - accessibleProjectIds, + allowedProjectIds, criteria.filters.id.$in, ); } else { - criteria.filters.id = { $in: accessibleProjectIds }; + criteria.filters.id = { $in: allowedProjectIds }; } return retrieveProjects(req, criteria, sort, req.query.fields); }) diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index c890619d..38e7d556 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -323,14 +323,22 @@ module.exports = [ models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { + let allowedProjectIds = accessibleProjectIds; + // get projects with pending invite for current user + const invites = models.ProjectMemberInvite.getProjectInvitesForUser( + req.authUser.email, + req.authUser.userId); + if (invites) { + allowedProjectIds = _.union(allowedProjectIds, invites); + } // filter based on accessible if (_.get(criteria.filters, 'id', null)) { criteria.filters.id.$in = _.intersection( - accessibleProjectIds, + allowedProjectIds, criteria.filters.id.$in, ); } else { - criteria.filters.id = { $in: accessibleProjectIds }; + criteria.filters.id = { $in: allowedProjectIds }; } return retrieveProjects(req, criteria, sort, req.query.fields); }) diff --git a/src/util.js b/src/util.js index 542c2ee1..3909fd95 100644 --- a/src/util.js +++ b/src/util.js @@ -18,7 +18,7 @@ import elasticsearch from 'elasticsearch'; import Promise from 'bluebird'; // import AWS from 'aws-sdk'; -import { ADMIN_ROLES, TOKEN_SCOPES } from './constants'; +import { ADMIN_ROLES, TOKEN_SCOPES, EVENT } from './constants'; const exec = require('child_process').exec; const models = require('./models').default; @@ -365,7 +365,8 @@ _.assignIn(util, { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - }).then(res => _.get(res, 'data.result.content', []).map(r => r.roleName)); + }).then(res => _.get(res, 'data.result.content', []) + .map(r => r.roleName)); } catch (err) { return Promise.reject(err); } @@ -384,6 +385,44 @@ _.assignIn(util, { return source; } }), + + /** + * Add userId to project + * @param {object} req Request object that should contain project info and user info + * @param {object} member the member to be added to project + */ + addUserToProject: Promise.coroutine(function* (req, member) { // eslint-disable-line + const members = req.context.currentProjectMembers; + + // check if member is already registered + const existingMember = _.find(members, m => m.userId === member.userId); + if (existingMember) { + const err = new Error(`User already registered for role: ${existingMember.role}`); + err.status = 400; + return Promise.reject(err); + } + + req.log.debug('creating member', member); + let newMember = null; + // register member + + return models.ProjectMember.create(member) + .then((_newMember) => { + newMember = _newMember.get({ plain: true }); + // publish event + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, + newMember, + { correlationId: req.id }, + ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, { req, member: newMember }); + return newMember; + }) + .catch((err) => { + req.log.error('Unable to register ', err); + return Promise.reject(err); + }); + }), }); export default util; diff --git a/swagger.yaml b/swagger.yaml index 8f081f9f..b547e16e 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1648,11 +1648,100 @@ paths: '204': description: Milestone template successfully removed - - - - - + /projects/{projectId}/members/invite: + get: + tags: + - project member invite + operationId: getCurrentUserInvite + security: + - Bearer: [] + description: Retrieve the invite for current user. + parameters: + - $ref: "#/parameters/projectIdParam" + responses: + '200': + description: The invite for current user + schema: + $ref: "#/definitions/ProjectMemberInviteResponse" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '400': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Invite not found + schema: + $ref: "#/definitions/ErrorModel" + '500': + description: Invalid server state or unknown error + schema: + $ref: "#/definitions/ErrorModel" + post: + tags: + - project member invite + operationId: addProjectMemberInvite + security: + - Bearer: [] + description: Create an invite. All users who can access this endpoint, however more restriction will be applied based on role to be added. + parameters: + - $ref: "#/parameters/projectIdParam" + - in: body + name: body + required: true + schema: + $ref: '#/definitions/AddProjectMemberInvitesRequest' + responses: + '201': + description: Returns the newly created invite + schema: + $ref: "#/definitions/ProjectMemberInviteResponse" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '400': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + '500': + description: Invalid server state or unknown error + schema: + $ref: "#/definitions/ErrorModel" + put: + tags: + - project member invite + operationId: updateProjectMemberInvite + security: + - Bearer: [] + description: Update an invite. All users who can access this endpoint, however more restriction will be applied based on role to be updated. + parameters: + - $ref: "#/parameters/projectIdParam" + - in: body + name: body + required: true + schema: + $ref: '#/definitions/UpdateProjectMemberInviteRequest' + responses: + '200': + description: Returns the newly updated invite + schema: + $ref: "#/definitions/ProjectMemberInviteResponse" + '400': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '500': + description: Invalid server state or unknown error + schema: + $ref: "#/definitions/ErrorModel" + parameters: projectIdParam: name: projectId @@ -3663,4 +3752,113 @@ definitions: type: array items: $ref: "#/definitions/ProductCategory" - \ No newline at end of file + + ProjectMemberInvite: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 + projectId: + description: unique project identifier + type: integer + format: int64 + userId: + type: integer + format: int64 + description: The user Id + email: + type: string + description: The user email + role: + description: The user role in the project + type: string + enum: ["manager", "customer", "copilot"] + status: + description: The invite status + type: string + enum: ["pending", "accepted", "refused", "canceled"] + createdAt: + type: string + description: Datetime (GMT) when task was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this task + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when task was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this task + readOnly: true + + AddProjectMemberInvitesRequest: + title: Add project member invites request object + type: object + properties: + param: + type: object + properties: + userIds: + description: The user Id list, could not present with emails + type: array + items: + type: integer + format: int64 + emails: + type: array + items: + type: string + description: The user email list, could not present with userIds + role: + description: The target role in the project + type: string + enum: ["manager", "customer", "copilot"] + + UpdateProjectMemberInviteRequest: + title: Update project member invite request object + type: object + properties: + param: + type: object + properties: + userId: + type: integer + format: int64 + description: The user Id, could not present with email + email: + type: string + description: The user email, could not present with userId + status: + description: The invite status + type: string + enum: ["pending", "accepted", "refused", "canceled"] + + ProjectMemberInviteResponse: + title: Project member invite response object + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/ProjectMemberInvite" + From 13f5dfdb6b1f125107eb61862cbc12398a9e2640 Mon Sep 17 00:00:00 2001 From: Samir Gondzetovic Date: Sun, 16 Dec 2018 11:47:53 +0000 Subject: [PATCH 05/50] update invite change --- src/models/projectMemberInvite.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/models/projectMemberInvite.js b/src/models/projectMemberInvite.js index 864217bb..c6f31b0c 100644 --- a/src/models/projectMemberInvite.js +++ b/src/models/projectMemberInvite.js @@ -58,7 +58,9 @@ module.exports = function defineProjectMemberInvite(sequelize, DataTypes) { getPendingInviteByEmailOrUserId(projectId, email, userId) { const where = { status: INVITE_STATUS.PENDING }; - if (email) { + if (email && userId) { + _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] }); + } else if (email) { _.assign(where, { email }); } else if (userId) { _.assign(where, { userId }); @@ -70,7 +72,9 @@ module.exports = function defineProjectMemberInvite(sequelize, DataTypes) { getProjectInvitesForUser(email, userId) { const where = { status: INVITE_STATUS.PENDING }; - if (email) { + if (email && userId) { + _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] }); + } else if (email) { _.assign(where, { email }); } else if (userId) { _.assign(where, { userId }); From 0aed2c522a8c584e7cc52984a80f7e61ea53e693 Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Wed, 26 Dec 2018 15:10:23 +0530 Subject: [PATCH 06/50] Added new field subCategory to productTemplates to support Addons --- .../20181226_productTemplates_subCategory.sql | 14 ++++++++++++++ src/models/productTemplate.js | 1 + src/routes/metadata/list.spec.js | 1 + src/routes/milestoneTemplates/clone.spec.js | 2 ++ src/routes/milestoneTemplates/create.spec.js | 2 ++ src/routes/milestoneTemplates/delete.spec.js | 2 ++ src/routes/milestoneTemplates/get.spec.js | 2 ++ src/routes/milestoneTemplates/list.spec.js | 2 ++ src/routes/milestoneTemplates/update.spec.js | 2 ++ src/routes/phases/create.spec.js | 1 + src/routes/productTemplates/create.js | 1 + src/routes/productTemplates/create.spec.js | 1 + src/routes/productTemplates/delete.spec.js | 1 + src/routes/productTemplates/get.spec.js | 1 + src/routes/productTemplates/list.spec.js | 7 +++++-- src/routes/productTemplates/update.spec.js | 1 + src/routes/projectUpgrade/create.spec.js | 1 + src/routes/projects/create.spec.js | 3 +++ src/routes/timelines/create.spec.js | 1 + src/tests/seed.js | 1 + 20 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 migrations/20181226_productTemplates_subCategory.sql diff --git a/migrations/20181226_productTemplates_subCategory.sql b/migrations/20181226_productTemplates_subCategory.sql new file mode 100644 index 00000000..f55056c9 --- /dev/null +++ b/migrations/20181226_productTemplates_subCategory.sql @@ -0,0 +1,14 @@ +-- +-- UPDATE EXISTING TABLES: +-- product_templates: +-- added column `subCategory` + +-- +-- product_templates + +-- Add new column +ALTER TABLE product_templates ADD COLUMN "subCategory" character varying(45); +-- Update new column +UPDATE projects SET "subCategory"="category" WHERE "subCategory" is NULL; +-- Set not null +ALTER TABLE projects ALTER COLUMN "subCategory" SET NOT NULL; diff --git a/src/models/productTemplate.js b/src/models/productTemplate.js index 4e4dc184..f8fd7f17 100644 --- a/src/models/productTemplate.js +++ b/src/models/productTemplate.js @@ -9,6 +9,7 @@ module.exports = (sequelize, DataTypes) => { name: { type: DataTypes.STRING(255), allowNull: false }, productKey: { type: DataTypes.STRING(45), allowNull: false }, category: { type: DataTypes.STRING(45), allowNull: false }, + subCategory: { type: DataTypes.STRING(45), allowNull: false }, icon: { type: DataTypes.STRING(255), allowNull: false }, brief: { type: DataTypes.STRING(45), allowNull: false }, details: { type: DataTypes.STRING(255), allowNull: false }, diff --git a/src/routes/metadata/list.spec.js b/src/routes/metadata/list.spec.js index 7e7c9b4f..86798d21 100644 --- a/src/routes/metadata/list.spec.js +++ b/src/routes/metadata/list.spec.js @@ -30,6 +30,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/milestoneTemplates/clone.spec.js b/src/routes/milestoneTemplates/clone.spec.js index 03ad322a..a0f9bbbd 100644 --- a/src/routes/milestoneTemplates/clone.spec.js +++ b/src/routes/milestoneTemplates/clone.spec.js @@ -14,6 +14,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -47,6 +48,7 @@ const productTemplates = [ name: 'name 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/create.spec.js b/src/routes/milestoneTemplates/create.spec.js index 00dad710..beacf71f 100644 --- a/src/routes/milestoneTemplates/create.spec.js +++ b/src/routes/milestoneTemplates/create.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/delete.spec.js b/src/routes/milestoneTemplates/delete.spec.js index 0d0d5527..92a6dcb6 100644 --- a/src/routes/milestoneTemplates/delete.spec.js +++ b/src/routes/milestoneTemplates/delete.spec.js @@ -40,6 +40,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -73,6 +74,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/get.spec.js b/src/routes/milestoneTemplates/get.spec.js index 58ce6a5a..50f31370 100644 --- a/src/routes/milestoneTemplates/get.spec.js +++ b/src/routes/milestoneTemplates/get.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/list.spec.js b/src/routes/milestoneTemplates/list.spec.js index 2ee2f25f..465a388f 100644 --- a/src/routes/milestoneTemplates/list.spec.js +++ b/src/routes/milestoneTemplates/list.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/update.spec.js b/src/routes/milestoneTemplates/update.spec.js index 68be6f31..3de8c430 100644 --- a/src/routes/milestoneTemplates/update.spec.js +++ b/src/routes/milestoneTemplates/update.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 8bdcb5d8..69f45a4d 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -97,6 +97,7 @@ describe('Project Phases', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/create.js b/src/routes/productTemplates/create.js index 8f47ec4f..9907d085 100644 --- a/src/routes/productTemplates/create.js +++ b/src/routes/productTemplates/create.js @@ -16,6 +16,7 @@ const schema = { param: Joi.object().keys({ id: Joi.any().strip(), category: Joi.string().max(45).required(), + subCategory: Joi.string().max(45).required(), name: Joi.string().max(255).required(), productKey: Joi.string().max(45).required(), icon: Joi.string().max(255).required(), diff --git a/src/routes/productTemplates/create.spec.js b/src/routes/productTemplates/create.spec.js index b4da9239..0c283caf 100644 --- a/src/routes/productTemplates/create.spec.js +++ b/src/routes/productTemplates/create.spec.js @@ -35,6 +35,7 @@ describe('CREATE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/delete.spec.js b/src/routes/productTemplates/delete.spec.js index edb53d74..5c4d177c 100644 --- a/src/routes/productTemplates/delete.spec.js +++ b/src/routes/productTemplates/delete.spec.js @@ -43,6 +43,7 @@ describe('DELETE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/get.spec.js b/src/routes/productTemplates/get.spec.js index 42b34e30..fd7cd7da 100644 --- a/src/routes/productTemplates/get.spec.js +++ b/src/routes/productTemplates/get.spec.js @@ -15,6 +15,7 @@ describe('GET product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js index 9282e409..a9386f66 100644 --- a/src/routes/productTemplates/list.spec.js +++ b/src/routes/productTemplates/list.spec.js @@ -14,12 +14,13 @@ import testUtil from '../../tests/util'; const validateProductTemplates = (count, resJson, expectedTemplates) => { resJson.should.have.length(count); resJson.forEach((pt, idx) => { - pt.should.have.all.keys('id', 'name', 'productKey', 'category', 'icon', 'brief', 'details', 'aliases', - 'template', 'disabled', 'hidden', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); + pt.should.have.all.keys('id', 'name', 'productKey', 'category', 'subCategory', 'icon', 'brief', 'details', + 'aliases', 'template', 'disabled', 'hidden', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); pt.should.not.have.all.keys('deletedAt', 'deletedBy'); pt.name.should.be.eql(expectedTemplates[idx].name); pt.productKey.should.be.eql(expectedTemplates[idx].productKey); pt.category.should.be.eql(expectedTemplates[idx].category); + pt.subCategory.should.be.eql(expectedTemplates[idx].subCategory); pt.icon.should.be.eql(expectedTemplates[idx].icon); pt.brief.should.be.eql(expectedTemplates[idx].brief); pt.details.should.be.eql(expectedTemplates[idx].details); @@ -38,6 +39,7 @@ describe('LIST product templates', () => { name: 'name 1', productKey: 'productKey-1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -73,6 +75,7 @@ describe('LIST product templates', () => { name: 'template 2', productKey: 'productKey-2', category: 'concrete', + subCategory: 'concrete', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/productTemplates/update.spec.js b/src/routes/productTemplates/update.spec.js index 6e65461d..0aeedaf6 100644 --- a/src/routes/productTemplates/update.spec.js +++ b/src/routes/productTemplates/update.spec.js @@ -15,6 +15,7 @@ describe('UPDATE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/projectUpgrade/create.spec.js b/src/routes/projectUpgrade/create.spec.js index 41ee7e0b..9ab136e0 100644 --- a/src/routes/projectUpgrade/create.spec.js +++ b/src/routes/projectUpgrade/create.spec.js @@ -95,6 +95,7 @@ describe('Project upgrade', () => { name: 'name 1', productKey: 'a product key', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 5810401e..e8807e38 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -38,6 +38,7 @@ describe('Project create', () => { name: 'template 1', productKey: 'productKey-1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon2.ico', brief: 'brief 1', details: 'details 1', @@ -51,6 +52,7 @@ describe('Project create', () => { name: 'template 2', productKey: 'productKey-2', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', @@ -64,6 +66,7 @@ describe('Project create', () => { name: 'template 3', productKey: 'productKey-3', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon3.ico', brief: 'brief 3', details: 'details 3', diff --git a/src/routes/timelines/create.spec.js b/src/routes/timelines/create.spec.js index 41590c37..20c88ed8 100644 --- a/src/routes/timelines/create.spec.js +++ b/src/routes/timelines/create.spec.js @@ -45,6 +45,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/tests/seed.js b/src/tests/seed.js index 5c4f553d..8790ad96 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -326,6 +326,7 @@ models.sequelize.sync({ force: true }) name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', question: 'question 1', info: 'info 1', From b3de8d0d44dd261c217d576b1d180e611f89398b Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Wed, 26 Dec 2018 15:46:36 +0530 Subject: [PATCH 07/50] Corrected table name in migration script. --- migrations/20181226_productTemplates_subCategory.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20181226_productTemplates_subCategory.sql b/migrations/20181226_productTemplates_subCategory.sql index f55056c9..8626b0e2 100644 --- a/migrations/20181226_productTemplates_subCategory.sql +++ b/migrations/20181226_productTemplates_subCategory.sql @@ -9,6 +9,6 @@ -- Add new column ALTER TABLE product_templates ADD COLUMN "subCategory" character varying(45); -- Update new column -UPDATE projects SET "subCategory"="category" WHERE "subCategory" is NULL; +UPDATE product_templates SET "subCategory"="category" WHERE "subCategory" is NULL; -- Set not null -ALTER TABLE projects ALTER COLUMN "subCategory" SET NOT NULL; +ALTER TABLE product_templates ALTER COLUMN "subCategory" SET NOT NULL; From ec2577b867b1ac1ee52528c0175f19206ec71fac Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 27 Dec 2018 11:04:13 +0800 Subject: [PATCH 08/50] fix validation for productTemplates update endpoint --- src/routes/productTemplates/update.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/productTemplates/update.js b/src/routes/productTemplates/update.js index 82095299..39456144 100644 --- a/src/routes/productTemplates/update.js +++ b/src/routes/productTemplates/update.js @@ -21,10 +21,11 @@ const schema = { name: Joi.string().max(255), productKey: Joi.string().max(45), category: Joi.string().max(45), + subCategory: Joi.string().max(45), icon: Joi.string().max(255), brief: Joi.string().max(45), details: Joi.string().max(255), - aliases: Joi.object(), + aliases: Joi.array(), template: Joi.object(), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), From 7be5db319e00425b086c1ef6464723473b5921a3 Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Fri, 28 Dec 2018 14:24:52 +0530 Subject: [PATCH 09/50] Fixed testcase for productTemplate PATCH calls. --- src/routes/productTemplates/update.spec.js | 29 +++------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/routes/productTemplates/update.spec.js b/src/routes/productTemplates/update.spec.js index 0aeedaf6..f0223d4b 100644 --- a/src/routes/productTemplates/update.spec.js +++ b/src/routes/productTemplates/update.spec.js @@ -19,13 +19,7 @@ describe('UPDATE product template', () => { icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', - aliases: { - alias1: { - subAlias1A: 1, - subAlias1B: 2, - }, - alias2: [1, 2, 3], - }, + aliases: ['productTemplate-1', 'productTemplate_1'], disabled: true, hidden: true, template: { @@ -87,17 +81,11 @@ describe('UPDATE product template', () => { name: 'template 1 - update', productKey: 'productKey 1 - update', category: 'concrete', + subCategory: 'concrete', icon: 'http://example.com/icon1-update.ico', brief: 'brief 1 - update', details: 'details 1 - update', - aliases: { - alias1: { - subAlias1A: 11, - subAlias1C: 'new', - }, - alias2: [4], - alias3: 'new', - }, + aliases: ['productTemplate-1-update', 'productTemplate_1-update'], template: { template1: { name: 'template 1 - update', @@ -214,16 +202,7 @@ describe('UPDATE product template', () => { resJson.details.should.be.eql(body.param.details); resJson.disabled.should.be.eql(true); resJson.hidden.should.be.eql(true); - - resJson.aliases.should.be.eql({ - alias1: { - subAlias1A: 11, - subAlias1B: 2, - subAlias1C: 'new', - }, - alias2: [4], - alias3: 'new', - }); + resJson.aliases.should.be.eql(body.param.aliases); resJson.template.should.be.eql({ template1: { name: 'template 1 - update', From 3e97ee49b632b2aaa64b7c441261bc226ddcca98 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 02:09:49 +0100 Subject: [PATCH 10/50] integration changes --- local/mock-services/server.js | 7 +++++++ src/models/projectMemberInvite.js | 2 +- src/routes/projectMembers/update.js | 2 +- src/routes/projects/list.js | 19 +++++++++++++++---- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/local/mock-services/server.js b/local/mock-services/server.js index 550f276a..c1f0069f 100644 --- a/local/mock-services/server.js +++ b/local/mock-services/server.js @@ -44,6 +44,7 @@ server.get('/v3/members/_search', (req, res) => { return ret; }); const userIds = _.map(criteria, 'userId'); + const handles = _.map(criteria, 'handle'); const cloned = _.cloneDeep(members); const response = { id: 'res1', @@ -59,6 +60,12 @@ server.get('/v3/members/_search', (req, res) => { found = _.pick(found, fields); } return found; + } else if (_.indexOf(handles, single.result.content.handle) > -1) { + let found = single.result.content; + if (fields.length > 0) { + found = _.pick(found, fields); + } + return found; } return null; }).filter(_.identity); diff --git a/src/models/projectMemberInvite.js b/src/models/projectMemberInvite.js index c6f31b0c..ac33ee2d 100644 --- a/src/models/projectMemberInvite.js +++ b/src/models/projectMemberInvite.js @@ -56,7 +56,7 @@ module.exports = function defineProjectMemberInvite(sequelize, DataTypes) { }); }, getPendingInviteByEmailOrUserId(projectId, email, userId) { - const where = { status: INVITE_STATUS.PENDING }; + const where = { projectId, status: INVITE_STATUS.PENDING }; if (email && userId) { _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] }); diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 081fd614..97efadb1 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -17,7 +17,7 @@ const updateProjectMemberValdiations = { param: Joi.object().keys({ isPrimary: Joi.boolean(), role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, - PROJECT_MEMBER_ROLE.COPILOT).required(), + PROJECT_MEMBER_ROLE.COPILOT, PROJECT_MEMBER_ROLE.OBSERVER).required(), }), }, }; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 38e7d556..331a27c1 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -30,6 +30,10 @@ const PROJECT_MEMBER_ATTRIBUTES = _.without( _.keys(models.ProjectMember.rawAttributes), 'deletedAt', ); +const PROJECT_MEMBER_INVITE_ATTRIBUTES = _.without( + _.keys(models.ProjectMemberInvite.rawAttributes), + 'deletedAt' +); const PROJECT_ATTACHMENT_ATTRIBUTES = _.without( _.keys(models.ProjectAttachment.rawAttributes), 'deletedAt', @@ -126,6 +130,10 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { const memberFields = _.get(fields, 'project_members'); sourceInclude = sourceInclude.concat(_.map(memberFields, single => `members.${single}`)); } + if (_.get(fields, 'project_member_invites', null)) { + const memberFields = _.get(fields, 'project_member_invites'); + sourceInclude = sourceInclude.concat(_.map(memberFields, single => `invites.${single}`)); + } if (_.get(fields, 'project_phases', null)) { const phaseFields = _.get(fields, 'project_phases'); sourceInclude = sourceInclude.concat(_.map(phaseFields, single => `phases.${single}`)); @@ -249,6 +257,7 @@ const retrieveProjects = (req, criteria, sort, ffields) => { fields = util.parseFields(fields, { projects: PROJECT_ATTRIBUTES, project_members: PROJECT_MEMBER_ATTRIBUTES, + project_member_invites: PROJECT_MEMBER_INVITE_ATTRIBUTES, project_phases: PROJECT_PHASE_ATTRIBUTES, project_phases_products: PROJECT_PHASE_PRODUCTS_ATTRIBUTES, attachments: PROJECT_ATTACHMENT_ATTRIBUTES, @@ -321,16 +330,18 @@ module.exports = [ const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + return getProjectIds .then((accessibleProjectIds) => { - let allowedProjectIds = accessibleProjectIds; + const allowedProjectIds = accessibleProjectIds; // get projects with pending invite for current user const invites = models.ProjectMemberInvite.getProjectInvitesForUser( req.authUser.email, req.authUser.userId); - if (invites) { - allowedProjectIds = _.union(allowedProjectIds, invites); - } + + return invites.then((ids => _.union(allowedProjectIds, ids))); + }) + .then((allowedProjectIds) => { // filter based on accessible if (_.get(criteria.filters, 'id', null)) { criteria.filters.id.$in = _.intersection( From 95595e1970377a9b6272e46e9e088d9de7c4a5c0 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 03:36:42 +0100 Subject: [PATCH 11/50] return invites in GET /projects/:id --- src/routes/projects/get.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index 7138571e..a6f8cabe 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -64,6 +64,10 @@ module.exports = [ if (attachments) { project.attachments = attachments; } + return models.ProjectMemberInvite.getPendingInvitesForProject(projectId); + }) + .then((invites) => { + project.invites = invites; res.status(200).json(util.wrapResponse(req.id, project)); }) .catch(err => next(err)); From d38c9d178d6079f873c55be49c92cd6baef4deed Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 12:28:39 +0100 Subject: [PATCH 12/50] ignore duplicate member invites; fix invite remove error --- src/events/projectMembers/index.js | 3 +- src/routes/projectMemberInvites/create.js | 37 ++++------------------- src/routes/projectMemberInvites/get.js | 4 +-- src/routes/projectMemberInvites/update.js | 15 ++------- src/routes/projects/list.js | 2 +- 5 files changed, 14 insertions(+), 47 deletions(-) diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index 82510b7b..e25eb7a2 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -88,7 +88,8 @@ const projectMemberAddedHandler = Promise.coroutine(function* a(logger, msg, cha members.push(payload); // now merge the updated changes and reindex the document for invites const invites = _.isArray(doc._source.invites) ? doc._source.invites : []; // eslint-disable-line no-underscore-dangle - _.remove(invites, invite => invite === payload.email || invite === payload.userId); + // removing any invites for the member just added to the team + _.remove(invites, invite => invite.email === payload.email || invite.userId === payload.userId); return _.merge(doc._source, { members, invites }); // eslint-disable-line no-underscore-dangle }); yield Promise.all([directUpdatePromise(), updateESPromise(logger, origRequestId, projectId, updateDocPromise)]); diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index d56fc2f7..14d31052 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -49,6 +49,8 @@ module.exports = [ const promises = []; if (invite.userIds) { + // remove members already in the team + _.remove(invite.userIds, u => _.some(members, m => m.userId === u)); // permission: // user has to have constants.MANAGER_ROLES role // to be invited as PROJECT_MEMBER_ROLE.MANAGER @@ -58,19 +60,6 @@ module.exports = [ promises.push(util.getUserRoles(userId, req.log, req.id)); }); } - - // validate each userId is not already a member - const alreadyMembers = []; - _.forEach(members, (member) => { - if (invite.userIds.includes(member.userId)) { - alreadyMembers.push(member.userId); - } - }); - if (alreadyMembers.length > 0) { - const err = new Error(`${alreadyMembers.join()} are already members of project ${projectId}`); - err.status = 400; - return next(err); - } } if (invite.emails) { @@ -104,24 +93,6 @@ module.exports = [ } return models.ProjectMemberInvite.getPendingInvitesForProject(projectId) .then((invites) => { - req.log.debug('Chekcing if user has been invited'); - // validate for each userId/email there is no existing invitation - const alreadyInvites = []; - _.forEach(invites, (i) => { - if (invite.userIds) { - if (invite.userIds.includes(i.userId)) { - alreadyInvites.push(i.userId); - } - } else if (invite.emails.includes(i.email)) { - alreadyInvites.push(i.email); - } - }); - if (alreadyInvites.length > 0) { - const err = new Error(`${alreadyInvites.join()} are already invited`); - err.status = 400; - return next(err); - } - const data = { projectId, role: invite.role, @@ -131,6 +102,8 @@ module.exports = [ }; const invitePromises = []; if (invite.userIds) { + // remove invites for users that are invited already + _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); invite.userIds.forEach((userId) => { const dataNew = _.clone(data); _.assign(dataNew, { @@ -142,6 +115,8 @@ module.exports = [ data.userId = null; if (invite.emails) { + // remove invites for users that are invited already + _.remove(invite.emails, u => _.some(invites, i => i.email === u)); invite.emails.forEach((email) => { const dataNew = _.clone(data); _.assign(dataNew, { diff --git a/src/routes/projectMemberInvites/get.js b/src/routes/projectMemberInvites/get.js index 84ce2366..8de0fd5a 100644 --- a/src/routes/projectMemberInvites/get.js +++ b/src/routes/projectMemberInvites/get.js @@ -18,14 +18,14 @@ module.exports = [ const projectId = _.parseInt(req.params.projectId); const currentUserId = req.authUser.userId; let invite; - return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, null, currentUserId) + return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, req.authUser.email, currentUserId) .then((_invite) => { invite = _invite; if (!invite) { // check there is an existing invite for the user with status PENDING // handle 404 const err = new Error('invite not found for project id ' + - `${projectId}, userId ${currentUserId}`); + `${projectId}, userId ${currentUserId}, email ${req.authUser.email}`); err.status = 404; return next(err); } diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 47da01cd..5e6f5866 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -4,7 +4,7 @@ import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, USER_ROLE, INVITE_STATUS, EVENT } from '../../constants'; +import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, INVITE_STATUS, EVENT } from '../../constants'; /** * API to update invite member to project. @@ -36,13 +36,6 @@ module.exports = [ const putInvite = req.body.param; const projectId = _.parseInt(req.params.projectId); - // not userId and email at the same time - if (!!putInvite.userId && !!putInvite.email) { - const err = new Error('userId and email cannot be presented in the same request'); - err.status = 400; - return next(err); - } - // userId or email should be provided if (!putInvite.userId && !putInvite.email) { const err = new Error('userId or email should be provided'); @@ -70,9 +63,7 @@ module.exports = [ req.log.debug('Chekcing user permission for updating invite'); let error = null; if (putInvite.status === INVITE_STATUS.CANCELED) { - if (util.hasRole(req, USER_ROLE.COPILOT) && invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { - error = `Copilot can cancel invites only for ${PROJECT_MEMBER_ROLE.CUSTOMER}`; - } else if (!util.hasRoles(req, MANAGER_ROLES)) { + if (!util.hasRoles(req, MANAGER_ROLES) && invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { error = `Project members can cancel invites only for ${PROJECT_MEMBER_ROLE.CUSTOMER}`; } } else if (!!putInvite.userId && putInvite.userId !== req.authUser.userId) { @@ -111,7 +102,7 @@ module.exports = [ const member = { projectId, role: updatedInvite.role, - userId: updatedInvite.userId, + userId: req.authUser.userId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, }; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 331a27c1..1e490f57 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -32,7 +32,7 @@ const PROJECT_MEMBER_ATTRIBUTES = _.without( ); const PROJECT_MEMBER_INVITE_ATTRIBUTES = _.without( _.keys(models.ProjectMemberInvite.rawAttributes), - 'deletedAt' + 'deletedAt', ); const PROJECT_ATTACHMENT_ATTRIBUTES = _.without( _.keys(models.ProjectAttachment.rawAttributes), From 642862070251c9b85a3967013f03c13b17665945 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 12:32:48 +0100 Subject: [PATCH 13/50] fix tests --- .../projectMemberInvites/create.spec.js | 89 ------------------- .../projectMemberInvites/update.spec.js | 29 ------ 2 files changed, 118 deletions(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 90473c4c..0d242111 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -16,7 +16,6 @@ const should = chai.should(); describe('Project Member Invite create', () => { let project1; let project2; - let invite1; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -184,50 +183,6 @@ describe('Project Member Invite create', () => { }); }); - it('should return 400 if user has a pending invite', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: [{ - roleName: USER_ROLE.COPILOT, - }], - }, - }, - }), - }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/invite`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - userIds: [invite1.userId], - role: 'customer', - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - res.body.result.status.should.equal(400); - const errorMessage = _.get(resJson, 'message', ''); - sinon.assert.match(errorMessage, /.*already invited/); - done(); - } - }); - }); - it('should return 403 if try to create copilot with MEMBER', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ @@ -466,50 +421,6 @@ describe('Project Member Invite create', () => { } }); }); - it('should return 400 if already in the project', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: [{ - roleName: USER_ROLE.MANAGER, - }], - }, - }, - }), - }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .post(`/v4/projects/${project1.id}/members/invite`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - userIds: [40051332], - role: 'manager', - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - res.body.result.status.should.equal(400); - const errorMessage = _.get(resJson, 'message', ''); - sinon.assert.match(errorMessage, /.*are already members of project/); - done(); - } - }); - }); describe('Bus api', () => { let createEventSpy; diff --git a/src/routes/projectMemberInvites/update.spec.js b/src/routes/projectMemberInvites/update.spec.js index 9dab290b..be13b2e5 100644 --- a/src/routes/projectMemberInvites/update.spec.js +++ b/src/routes/projectMemberInvites/update.spec.js @@ -153,35 +153,6 @@ describe('Project member invite update', () => { }); }); - it('should return 400 if userId and email are presented the same time', (done) => { - request(server) - .put(`/v4/projects/${project1.id}/members/invite`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ - param: { - userId: 40051332, - email: 'hello@world.com', - status: INVITE_STATUS.CANCELED, - }, - }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - } else { - const resJson = res.body.result.content; - should.exist(resJson); - res.body.result.status.should.equal(400); - const errorMessage = _.get(resJson, 'message', ''); - sinon.assert.match(errorMessage, /.*userId and email cannot be presented in the same request/); - done(); - } - }); - }); - it('should return 403 if try to update MANAGER role invite with copilot', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ From a59f53526561fd8346818b53a030aa9ef52b5821 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 12:35:30 +0100 Subject: [PATCH 14/50] lint fix --- src/routes/projectMemberInvites/create.spec.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 0d242111..b688609a 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -66,10 +66,7 @@ describe('Project Member Invite create', () => { updatedBy: 1, createdAt: '2016-06-30 00:33:07+00', updatedAt: '2016-06-30 00:33:07+00', - }).then((in1) => { - invite1 = in1.get({ - plain: true, - }); + }).then(() => { done(); }); })); From 0d29c217a3e86de98c774b7fcc4758e2308e688c Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 12:58:56 +0100 Subject: [PATCH 15/50] fix tests --- src/routes/projectMemberInvites/create.spec.js | 4 ++-- src/routes/projectMemberInvites/update.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index b688609a..511ed69f 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -101,12 +101,12 @@ describe('Project Member Invite create', () => { }, }) .expect('Content-Type', /json/) - .expect(400) + .expect(201) .end((err, res) => { if (err) { done(err); } else { - res.body.result.status.should.equal(400); + res.body.result.status.should.equal(201); done(); } }); diff --git a/src/routes/projectMemberInvites/update.spec.js b/src/routes/projectMemberInvites/update.spec.js index be13b2e5..c69a272c 100644 --- a/src/routes/projectMemberInvites/update.spec.js +++ b/src/routes/projectMemberInvites/update.spec.js @@ -192,7 +192,7 @@ describe('Project member invite update', () => { should.exist(resJson); res.body.result.status.should.equal(403); const errorMessage = _.get(resJson, 'message', ''); - sinon.assert.match(errorMessage, /.*Copilot can cancel invites only for/); + sinon.assert.match(errorMessage, /.*Project members can cancel invites only for customer/); done(); } }); From 01fabdea323d3b5eb14bb571f7f9692989168294 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 13:38:02 +0100 Subject: [PATCH 16/50] fix update role permission --- src/routes/projectMemberInvites/update.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 5e6f5866..300ba0ad 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -66,7 +66,8 @@ module.exports = [ if (!util.hasRoles(req, MANAGER_ROLES) && invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { error = `Project members can cancel invites only for ${PROJECT_MEMBER_ROLE.CUSTOMER}`; } - } else if (!!putInvite.userId && putInvite.userId !== req.authUser.userId) { + } else if ((!!putInvite.userId && putInvite.userId !== req.authUser.userId) || + (!!putInvite.email && putInvite.email !== req.authUser.email)) { error = 'Project members can only update invites for themselves'; } From 031a600ab406cb8bf7bdf08e7611778915fdfc58 Mon Sep 17 00:00:00 2001 From: Samir Date: Thu, 3 Jan 2019 13:55:20 +0100 Subject: [PATCH 17/50] fix tests --- src/routes/projectMemberInvites/update.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/update.spec.js b/src/routes/projectMemberInvites/update.spec.js index c69a272c..a137e57e 100644 --- a/src/routes/projectMemberInvites/update.spec.js +++ b/src/routes/projectMemberInvites/update.spec.js @@ -223,7 +223,7 @@ describe('Project member invite update', () => { }) .send({ param: { - userId: invite1.userId, + userId: invite2.userId, status: INVITE_STATUS.CANCELED, }, }) From 39541c4caf7cbfee745d5112c787b702381770df Mon Sep 17 00:00:00 2001 From: Samir Date: Mon, 7 Jan 2019 01:22:36 +0100 Subject: [PATCH 18/50] send email event for email invites --- config/default.json | 5 +++- src/constants.js | 1 + src/routes/projectMemberInvites/create.js | 32 +++++++++++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/config/default.json b/config/default.json index a23a86e8..ff0ddaed 100644 --- a/config/default.json +++ b/config/default.json @@ -51,5 +51,8 @@ "AUTH0_URL": "", "TOKEN_CACHE_TIME": "", "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", - "AUTH0_PROXY_SERVER_URL" : "" + "AUTH0_PROXY_SERVER_URL" : "", + "EMAIL_INVITE_FROM_NAME":"Topcoder Connect", + "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com" + } diff --git a/src/constants.js b/src/constants.js index 47d8c9ce..8104e2a3 100644 --- a/src/constants.js +++ b/src/constants.js @@ -131,6 +131,7 @@ export const BUS_API_EVENT = { // Project Member Invites PROJECT_MEMBER_INVITE_CREATED: 'notifications.connect.project.member.invite.created', PROJECT_MEMBER_INVITE_UPDATED: 'notifications.connect.project.member.invite.updated', + PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.project.membet.invite.created', }; export const REGEX = { diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 14d31052..6619b44a 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -3,11 +3,14 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; +import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; import { PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES, - MANAGER_ROLES, INVITE_STATUS, EVENT } from '../../constants'; + MANAGER_ROLES, INVITE_STATUS, EVENT, PROJECT_MEMBER_EMAIL_INVITE_CREATED } from '../../constants'; +import { createEvent } from '../../services/busApi'; + /** * API to create member invite to project. @@ -140,8 +143,33 @@ module.exports = [ v, { correlationId: req.id }, ); + // send email invite (async) + if (v.email) { + models.Project + .find({ + where: { id: projectId }, + raw: true, + }) + .then((_project) => { + createEvent(PROJECT_MEMBER_EMAIL_INVITE_CREATED, + { + data: { + date: (new Date()).toISOString(), + projectName: _project.name, + projectId, + }, + recipients: [v.email], + version: 'v3', + from: { + name: config.get('EMAIL_INVITE_FROM_NAME'), + email: config.get('EMAIL_INVITE_FROM_EMAIL'), + }, + categories: [PROJECT_MEMBER_EMAIL_INVITE_CREATED], + }, req.log); + }); + } + return res.status(201).json(util.wrapResponse(req.id, values, null, 201)); }); - return res.status(201).json(util.wrapResponse(req.id, values, null, 201)); }); }); }).catch(err => next(err)); From a6ff782e201cd68ad1d22f0a90ab03ee4ef29174 Mon Sep 17 00:00:00 2001 From: Samir Date: Mon, 7 Jan 2019 01:29:41 +0100 Subject: [PATCH 19/50] fix tests --- src/routes/projectMemberInvites/create.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 511ed69f..4dabe90d 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -513,7 +513,7 @@ describe('Project Member Invite create', () => { done(err); } else { testUtil.wait(() => { - createEventSpy.calledOnce.should.be.true; + createEventSpy.calledTwice.should.be.true; createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({ projectId: project1.id, userId: null, From 19b4774d2581bd1dd35c2ea9a92ac933bb9090fc Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Mon, 7 Jan 2019 11:09:37 +0530 Subject: [PATCH 20/50] Fixed typo in event naming. --- src/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 8104e2a3..64b994cf 100644 --- a/src/constants.js +++ b/src/constants.js @@ -131,7 +131,7 @@ export const BUS_API_EVENT = { // Project Member Invites PROJECT_MEMBER_INVITE_CREATED: 'notifications.connect.project.member.invite.created', PROJECT_MEMBER_INVITE_UPDATED: 'notifications.connect.project.member.invite.updated', - PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.project.membet.invite.created', + PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.project.member.invite.created', }; export const REGEX = { From 581e640c5884c85be29271b9fe63999e8d4db7ae Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Mon, 7 Jan 2019 12:34:45 +0530 Subject: [PATCH 21/50] Corrected the migration script --- migrations/20181201_create_project_member_invites.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20181201_create_project_member_invites.sql b/migrations/20181201_create_project_member_invites.sql index a67df339..d2aafe30 100644 --- a/migrations/20181201_create_project_member_invites.sql +++ b/migrations/20181201_create_project_member_invites.sql @@ -29,7 +29,7 @@ CREATE SEQUENCE project_member_invites_id_seq NO MAXVALUE CACHE 1; -ALTER TABLE project_member_invites_id_seq OWNER BY project_member_invites.id; +ALTER SEQUENCE project_member_invites_id_seq OWNED BY project_member_invites.id; ALTER TABLE ONLY project_member_invites ADD CONSTRAINT "project_member_invites_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES projects(id) ON UPDATE CASCADE ON DELETE CASCADE; \ No newline at end of file From 40b823d81fe7d1eb84ec1a8bd20597be482fd0ce Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 14:55:14 +0530 Subject: [PATCH 22/50] Fixed the constant import --- src/routes/projectMemberInvites/create.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 6619b44a..fd1cb908 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -8,7 +8,7 @@ import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; import { PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES, - MANAGER_ROLES, INVITE_STATUS, EVENT, PROJECT_MEMBER_EMAIL_INVITE_CREATED } from '../../constants'; + MANAGER_ROLES, INVITE_STATUS, EVENT, BUS_API_EVENT } from '../../constants'; import { createEvent } from '../../services/busApi'; @@ -151,7 +151,7 @@ module.exports = [ raw: true, }) .then((_project) => { - createEvent(PROJECT_MEMBER_EMAIL_INVITE_CREATED, + createEvent(BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED, { data: { date: (new Date()).toISOString(), From 8f1ac5ccf04ef2c8b098d3b79ad87bdf46a0a357 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 15:18:15 +0530 Subject: [PATCH 23/50] lint fix --- src/routes/projectMemberInvites/create.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index fd1cb908..c9ec25fe 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -130,6 +130,7 @@ module.exports = [ } req.log.debug('Creating invites'); + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED return models.sequelize.Promise.all(invitePromises) .then((values) => { values.forEach((v) => { @@ -151,7 +152,7 @@ module.exports = [ raw: true, }) .then((_project) => { - createEvent(BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED, + createEvent(emailEventType, { data: { date: (new Date()).toISOString(), @@ -164,7 +165,7 @@ module.exports = [ name: config.get('EMAIL_INVITE_FROM_NAME'), email: config.get('EMAIL_INVITE_FROM_EMAIL'), }, - categories: [PROJECT_MEMBER_EMAIL_INVITE_CREATED], + categories: [`${config.ENV}:${emailEventType}`.toLowerCase()], }, req.log); }); } From 31c2a6417ae9522656b7c75e4654f3f6bdd9e3e3 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 15:19:21 +0530 Subject: [PATCH 24/50] lint fix --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index c9ec25fe..b6dfe291 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -130,7 +130,7 @@ module.exports = [ } req.log.debug('Creating invites'); - const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; return models.sequelize.Promise.all(invitePromises) .then((values) => { values.forEach((v) => { From f564ac19e51746c1d0b001732d76174ad80ffb78 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 16:04:16 +0530 Subject: [PATCH 25/50] fixing possible error in log statements --- src/services/busApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/busApi.js b/src/services/busApi.js index ddc58656..8a054fe8 100644 --- a/src/services/busApi.js +++ b/src/services/busApi.js @@ -54,7 +54,7 @@ function createEvent(topic, payload, logger) { payload, }).then((resp) => { logger.debug('Sent event to bus-api'); - logger.debug(`Sent event to bus-api [data]: ${resp.data}`); + logger.debug(`Sent event to bus-api [data]: ${resp}`); logger.debug(`Sent event to bus-api [status]: ${resp.status}`); }).catch((error) => { logger.debug('Error sending event to bus-api'); From 9273601e5a990b49017b41ee99dad508aa072c86 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 16:23:43 +0530 Subject: [PATCH 26/50] fixing possible error in log statements --- src/services/busApi.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/busApi.js b/src/services/busApi.js index 8a054fe8..0197523c 100644 --- a/src/services/busApi.js +++ b/src/services/busApi.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import config from 'config'; const Promise = require('bluebird'); @@ -54,8 +55,8 @@ function createEvent(topic, payload, logger) { payload, }).then((resp) => { logger.debug('Sent event to bus-api'); - logger.debug(`Sent event to bus-api [data]: ${resp}`); - logger.debug(`Sent event to bus-api [status]: ${resp.status}`); + logger.debug(`Sent event to bus-api [data]: ${_.get(resp, 'data')}`); + logger.debug(`Sent event to bus-api [status]: ${_.get(resp, 'status')}`); }).catch((error) => { logger.debug('Error sending event to bus-api'); if (error.response) { From 59927f6eb71fb13f6b10548c77bd7539df045076 Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Mon, 7 Jan 2019 16:34:31 +0530 Subject: [PATCH 27/50] Config changes and migration script correction --- config/default.json | 2 +- migrations/20181201_create_project_member_invites.sql | 6 ++++++ src/constants.js | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/config/default.json b/config/default.json index ff0ddaed..d01a2505 100644 --- a/config/default.json +++ b/config/default.json @@ -52,7 +52,7 @@ "TOKEN_CACHE_TIME": "", "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", "AUTH0_PROXY_SERVER_URL" : "", - "EMAIL_INVITE_FROM_NAME":"Topcoder Connect", + "EMAIL_INVITE_FROM_NAME":"Topcoder", "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com" } diff --git a/migrations/20181201_create_project_member_invites.sql b/migrations/20181201_create_project_member_invites.sql index d2aafe30..64b350f8 100644 --- a/migrations/20181201_create_project_member_invites.sql +++ b/migrations/20181201_create_project_member_invites.sql @@ -22,6 +22,9 @@ CREATE TABLE project_member_invites ( "deletedBy" bigint ); +ALTER TABLE ONLY project_member_invites + ADD CONSTRAINT project_member_invites_pkey PRIMARY KEY (id); + CREATE SEQUENCE project_member_invites_id_seq START WITH 1 INCREMENT BY 1 @@ -31,5 +34,8 @@ CREATE SEQUENCE project_member_invites_id_seq ALTER SEQUENCE project_member_invites_id_seq OWNED BY project_member_invites.id; +ALTER TABLE project_member_invites + ALTER COLUMN id SET DEFAULT nextval('project_member_invites_id_seq'); + ALTER TABLE ONLY project_member_invites ADD CONSTRAINT "project_member_invites_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES projects(id) ON UPDATE CASCADE ON DELETE CASCADE; \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 64b994cf..032e1e8d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -131,7 +131,7 @@ export const BUS_API_EVENT = { // Project Member Invites PROJECT_MEMBER_INVITE_CREATED: 'notifications.connect.project.member.invite.created', PROJECT_MEMBER_INVITE_UPDATED: 'notifications.connect.project.member.invite.updated', - PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.project.member.invite.created', + PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.email.project.member.invite.created', }; export const REGEX = { From b340265053b28327ddd1c96f04d00bc02145d09a Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Mon, 7 Jan 2019 16:42:23 +0530 Subject: [PATCH 28/50] Added new env vars --- deploy.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 059b8165..d353e3d5 100755 --- a/deploy.sh +++ b/deploy.sh @@ -195,6 +195,14 @@ make_task_def(){ { "name": "AUTH0_PROXY_SERVER_URL", "value": "%s" + }, + { + "name": "EMAIL_INVITE_FROM_NAMEL", + "value": "%s" + }, + { + "name": "EMAIL_INVITE_FROM_EMAIL", + "value": "%s" } ], "portMappings": [ @@ -253,8 +261,10 @@ make_task_def(){ AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") + EMAIL_INVITE_FROM_NAME=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_NAME") + EMAIL_INVITE_FROM_EMAIL=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_EMAIL") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ From f0502eeaa434c857d757c9f20110f6a24d860b40 Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Mon, 7 Jan 2019 16:46:18 +0530 Subject: [PATCH 29/50] fixed typo --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index d353e3d5..cdc1db08 100755 --- a/deploy.sh +++ b/deploy.sh @@ -197,7 +197,7 @@ make_task_def(){ "value": "%s" }, { - "name": "EMAIL_INVITE_FROM_NAMEL", + "name": "EMAIL_INVITE_FROM_NAME", "value": "%s" }, { From 61903995077a620b05d0620ef12c1a632116e24b Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 7 Jan 2019 16:52:22 +0530 Subject: [PATCH 30/50] Fixed env value for category of email --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index b6dfe291..cb37a34b 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -165,7 +165,7 @@ module.exports = [ name: config.get('EMAIL_INVITE_FROM_NAME'), email: config.get('EMAIL_INVITE_FROM_EMAIL'), }, - categories: [`${config.ENV}:${emailEventType}`.toLowerCase()], + categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], }, req.log); }); } From e720e3c23891f79a6520203a1771749db9812c94 Mon Sep 17 00:00:00 2001 From: Samir Date: Tue, 8 Jan 2019 00:36:57 +0100 Subject: [PATCH 31/50] email invitation template data update --- config/default.json | 3 ++- src/routes/projectMemberInvites/create.js | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/config/default.json b/config/default.json index ff0ddaed..8165fed0 100644 --- a/config/default.json +++ b/config/default.json @@ -53,6 +53,7 @@ "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", "AUTH0_PROXY_SERVER_URL" : "", "EMAIL_INVITE_FROM_NAME":"Topcoder Connect", - "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com" + "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", + "CONNECT_URL":"https://connect.topcoder-dev.com" } diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 6619b44a..3b6ac812 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -154,9 +154,20 @@ module.exports = [ createEvent(PROJECT_MEMBER_EMAIL_INVITE_CREATED, { data: { - date: (new Date()).toISOString(), - projectName: _project.name, - projectId, + connectURL: config.get('CONNECT_URL'), + projects: [ + { + name: _project.name, + projectId, + sections: [ + { + EMAIL_INVITES: true, + projectName: _project.name, + projectId, + }, + ], + }, + ], }, recipients: [v.email], version: 'v3', From b217239250e76095fe3884407d0de76b04c87bd6 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 8 Jan 2019 11:29:34 +0530 Subject: [PATCH 32/50] More configurable variables in invite email --- config/custom-environment-variables.json | 5 ++++- config/default.json | 4 +++- deploy.sh | 18 ++++++++++++++++-- src/routes/projectMemberInvites/create.js | 4 +++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 6620f43c..f2381f18 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -47,5 +47,8 @@ "AUTH0_AUDIENCE": "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME" : "TOKEN_CACHE_TIME", "whitelistedOriginsForUserIdAuth": "WHITELISTED_ORIGINS_FOR_USERID_AUTH", - "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL" + "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL", + "connectUrl": "CONNECT_URL", + "accountsAppUrl": "ACCOUNTS_APP_URL", + "inviteEmailSubject": "INVITE_EMAIL_SUBJECT" } diff --git a/config/default.json b/config/default.json index c37ec92a..1aff090b 100644 --- a/config/default.json +++ b/config/default.json @@ -54,6 +54,8 @@ "AUTH0_PROXY_SERVER_URL" : "", "EMAIL_INVITE_FROM_NAME":"Topcoder", "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", - "CONNECT_URL":"https://connect.topcoder-dev.com" + "inviteEmailSubject": "You are invited to Topcoder", + "connectUrl":"https://connect.topcoder-dev.com", + "accountsAppUrl": "https://accounts.topcoder-dev.com" } diff --git a/deploy.sh b/deploy.sh index cdc1db08..ed443add 100755 --- a/deploy.sh +++ b/deploy.sh @@ -152,6 +152,14 @@ make_task_def(){ "name": "CONNECT_PROJECTS_URL", "value": "%s" }, + { + "name": "CONNECT_URL", + "value": "%s" + }, + { + "name": "ACCOUNTS_APP_URL", + "value": "%s" + }, { "name": "SEGMENT_ANALYTICS_KEY", "value": "%s" @@ -203,6 +211,10 @@ make_task_def(){ { "name": "EMAIL_INVITE_FROM_EMAIL", "value": "%s" + }, + { + "name": "INVITE_EMAIL_SUBJECT", + "value": "%s" } ], "portMappings": [ @@ -238,6 +250,8 @@ make_task_def(){ DIRECT_PROJECT_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_DIRECT_PROJECT_SERVICE_ENDPOINT") FILE_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_FILE_SERVICE_ENDPOINT") CONNECT_PROJECTS_URL=$(eval "echo \$${ENV}_CONNECT_PROJECTS_URL") + CONNECT_URL=$(eval "echo \$${ENV}_CONNECT_URL") + ACCOUNTS_APP_URL=$(eval "echo \$${ENV}_ACCOUNTS_APP_URL") SEGMENT_ANALYTICS_KEY=$(eval "echo \$${ENV}_SEGMENT_ANALYTICS_KEY") MESSAGE_SERVICE_URL=$(eval "echo \$${ENV}_MESSAGE_SERVICE_URL") if [ "$ENV" = "PROD" ]; then @@ -258,13 +272,13 @@ make_task_def(){ KAFKA_CLIENT_CERT_KEY=$(eval "echo \$${ENV}_KAFKA_CLIENT_CERT_KEY") KAFKA_GROUP_ID=$(eval "echo \$${ENV}_KAFKA_GROUP_ID") KAFKA_URL=$(eval "echo \$${ENV}_KAFKA_URL") - AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") EMAIL_INVITE_FROM_NAME=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_NAME") EMAIL_INVITE_FROM_EMAIL=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_EMAIL") + INVITE_EMAIL_SUBJECT=$(eval "echo \$${ENV}_INVITE_EMAIL_SUBJECT") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $INVITE_EMAIL_SUBJECT $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index e94b90c6..2dec1d9c 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -155,7 +155,9 @@ module.exports = [ createEvent(emailEventType, { data: { - connectURL: config.get('CONNECT_URL'), + connectURL: config.get('connectUrl'), + accountsAppURL: config.get('accountsAppUrl'), + subject: config.get("inviteEmailSubject"), projects: [ { name: _project.name, From 7c6d9c00a279e3df5122a886d68e95d46fa57d50 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 8 Jan 2019 11:58:40 +0530 Subject: [PATCH 33/50] Configurable invite email section title --- config/custom-environment-variables.json | 3 ++- config/default.json | 1 + deploy.sh | 7 ++++++- src/routes/projectMemberInvites/create.js | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index f2381f18..c807e407 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -50,5 +50,6 @@ "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL", "connectUrl": "CONNECT_URL", "accountsAppUrl": "ACCOUNTS_APP_URL", - "inviteEmailSubject": "INVITE_EMAIL_SUBJECT" + "inviteEmailSubject": "INVITE_EMAIL_SUBJECT", + "inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE" } diff --git a/config/default.json b/config/default.json index 1aff090b..2b0ac247 100644 --- a/config/default.json +++ b/config/default.json @@ -55,6 +55,7 @@ "EMAIL_INVITE_FROM_NAME":"Topcoder", "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", "inviteEmailSubject": "You are invited to Topcoder", + "inviteEmailSectionTitle": "Project Invitation", "connectUrl":"https://connect.topcoder-dev.com", "accountsAppUrl": "https://accounts.topcoder-dev.com" diff --git a/deploy.sh b/deploy.sh index ed443add..fa4d35c5 100755 --- a/deploy.sh +++ b/deploy.sh @@ -215,6 +215,10 @@ make_task_def(){ { "name": "INVITE_EMAIL_SUBJECT", "value": "%s" + }, + { + "name": "INVITE_EMAIL_SECTION_TITLE", + "value": "%s" } ], "portMappings": [ @@ -277,8 +281,9 @@ make_task_def(){ EMAIL_INVITE_FROM_NAME=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_NAME") EMAIL_INVITE_FROM_EMAIL=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_EMAIL") INVITE_EMAIL_SUBJECT=$(eval "echo \$${ENV}_INVITE_EMAIL_SUBJECT") + INVITE_EMAIL_SECTION_TITLE=$(eval "echo \$${ENV}_INVITE_EMAIL_SECTION_TITLE") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $INVITE_EMAIL_SUBJECT $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $INVITE_EMAIL_SUBJECT $INVITE_EMAIL_SECTION_TITLE $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 2dec1d9c..96657e4d 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -165,6 +165,7 @@ module.exports = [ sections: [ { EMAIL_INVITES: true, + title: config.get("inviteEmailSectionTitle"), projectName: _project.name, projectId, }, From 36600a5f6a16e10db1106422cc73198968f23256 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 8 Jan 2019 11:59:15 +0530 Subject: [PATCH 34/50] lint fixes --- src/routes/projectMemberInvites/create.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 96657e4d..a9e81744 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -157,7 +157,7 @@ module.exports = [ data: { connectURL: config.get('connectUrl'), accountsAppURL: config.get('accountsAppUrl'), - subject: config.get("inviteEmailSubject"), + subject: config.get('inviteEmailSubject'), projects: [ { name: _project.name, @@ -165,7 +165,7 @@ module.exports = [ sections: [ { EMAIL_INVITES: true, - title: config.get("inviteEmailSectionTitle"), + title: config.get('inviteEmailSectionTitle'), projectName: _project.name, projectId, }, From 92f3497a83fb518a98836bf30c7eac2b9bf0dc93 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 8 Jan 2019 12:36:34 +0530 Subject: [PATCH 35/50] Trying to fix the deploy script --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index fa4d35c5..187c5816 100755 --- a/deploy.sh +++ b/deploy.sh @@ -283,7 +283,7 @@ make_task_def(){ INVITE_EMAIL_SUBJECT=$(eval "echo \$${ENV}_INVITE_EMAIL_SUBJECT") INVITE_EMAIL_SECTION_TITLE=$(eval "echo \$${ENV}_INVITE_EMAIL_SECTION_TITLE") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" $INVITE_EMAIL_SUBJECT $INVITE_EMAIL_SECTION_TITLE $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" "$INVITE_EMAIL_SUBJECT" "$INVITE_EMAIL_SECTION_TITLE" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ From 48f1412313d5115f90170a40eb9ed1c14254714f Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Sat, 12 Jan 2019 10:54:45 +0530 Subject: [PATCH 36/50] =?UTF-8?q?Github=20issue#2742,=20[Team=20Management?= =?UTF-8?q?=202.0]=20Inviting=20already=20invited=20user=20fails=20without?= =?UTF-8?q?=20clear=20error=20message=20=E2=80=94=20Fixing=20the=20issue?= =?UTF-8?q?=20when=20only=20one=20user=20is=20being=20invited=20and=20is?= =?UTF-8?q?=20already=20invited=20to=20the=20team.=20It=20was=20never=20re?= =?UTF-8?q?turning=20the=20response=20in=20such=20case.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/projectMemberInvites/create.js | 176 +++++++++++----------- 1 file changed, 91 insertions(+), 85 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index a9e81744..98c3eb2d 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -95,98 +95,104 @@ module.exports = [ } } return models.ProjectMemberInvite.getPendingInvitesForProject(projectId) - .then((invites) => { - const data = { - projectId, - role: invite.role, - status: INVITE_STATUS.PENDING, - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - }; - const invitePromises = []; - if (invite.userIds) { - // remove invites for users that are invited already - _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); - invite.userIds.forEach((userId) => { - const dataNew = _.clone(data); - _.assign(dataNew, { - userId, - }); - invitePromises.push(models.ProjectMemberInvite.create(dataNew)); - }); - } - data.userId = null; - - if (invite.emails) { - // remove invites for users that are invited already - _.remove(invite.emails, u => _.some(invites, i => i.email === u)); - invite.emails.forEach((email) => { - const dataNew = _.clone(data); - _.assign(dataNew, { - email, - }); - invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + .then((invites) => { + const data = { + projectId, + role: invite.role, + status: INVITE_STATUS.PENDING, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }; + const invitePromises = []; + if (invite.userIds) { + // remove invites for users that are invited already + _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); + invite.userIds.forEach((userId) => { + const dataNew = _.clone(data); + _.assign(dataNew, { + userId, + }); + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + data.userId = null; + + if (invite.emails) { + // remove invites for users that are invited already + _.remove(invite.emails, u => _.some(invites, i => i.email === u)); + invite.emails.forEach((email) => { + const dataNew = _.clone(data); + _.assign(dataNew, { + email, + }); + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + + if (invitePromises.length === 0) { + return []; + } + + req.log.debug('Creating invites'); + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; + return models.sequelize.Promise.all(invitePromises) + .then((values) => { + values.forEach((v) => { + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, { + req, + userId: v.userId, + email: v.email, }); - } - - req.log.debug('Creating invites'); - const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; - return models.sequelize.Promise.all(invitePromises) - .then((values) => { - values.forEach((v) => { - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, { - req, - userId: v.userId, - email: v.email, - }); - req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, - v, - { correlationId: req.id }, - ); - // send email invite (async) - if (v.email) { - models.Project - .find({ - where: { id: projectId }, - raw: true, - }) - .then((_project) => { - createEvent(emailEventType, - { - data: { - connectURL: config.get('connectUrl'), - accountsAppURL: config.get('accountsAppUrl'), - subject: config.get('inviteEmailSubject'), - projects: [ + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, + v, + { correlationId: req.id }, + ); + // send email invite (async) + if (v.email) { + models.Project + .find({ + where: { id: projectId }, + raw: true, + }) + .then((_project) => { + createEvent(emailEventType, + { + data: { + connectURL: config.get('connectUrl'), + accountsAppURL: config.get('accountsAppUrl'), + subject: config.get('inviteEmailSubject'), + projects: [ + { + name: _project.name, + projectId, + sections: [ { - name: _project.name, + EMAIL_INVITES: true, + title: config.get('inviteEmailSectionTitle'), + projectName: _project.name, projectId, - sections: [ - { - EMAIL_INVITES: true, - title: config.get('inviteEmailSectionTitle'), - projectName: _project.name, - projectId, - }, - ], }, ], }, - recipients: [v.email], - version: 'v3', - from: { - name: config.get('EMAIL_INVITE_FROM_NAME'), - email: config.get('EMAIL_INVITE_FROM_EMAIL'), - }, - categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], - }, req.log); - }); - } - return res.status(201).json(util.wrapResponse(req.id, values, null, 201)); + ], + }, + recipients: [v.email], + version: 'v3', + from: { + name: config.get('EMAIL_INVITE_FROM_NAME'), + email: config.get('EMAIL_INVITE_FROM_EMAIL'), + }, + categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], + }, req.log); }); - }); + } + }); + return values; }); - }).catch(err => next(err)); + }); + }) + .then(values => res.status(201).json(util.wrapResponse(req.id, values, null, 201))) + .catch(err => next(err)); }, ]; From 12c675ea806dfdeb473a049925b587fdeb23b776 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Sat, 12 Jan 2019 10:55:18 +0530 Subject: [PATCH 37/50] Added initiator user id for fetching information about inviting user in notification emails --- src/events/busApi.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/events/busApi.js b/src/events/busApi.js index 953a0431..94edcff3 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -668,6 +668,7 @@ module.exports = (app, logger) => { projectId, userId, email, + initiatorUserId: req.authUser.userId, }, logger); }); @@ -681,6 +682,7 @@ module.exports = (app, logger) => { userId, email, status, + initiatorUserId: req.authUser.userId, }, logger); }); }; From 346c391e69e8dcfbb8edda2a3268040b1407d7e4 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Sat, 12 Jan 2019 11:07:29 +0530 Subject: [PATCH 38/50] =?UTF-8?q?Github=20issue#2742,=20[Team=20Management?= =?UTF-8?q?=202.0]=20Inviting=20already=20invited=20user=20fails=20without?= =?UTF-8?q?=20clear=20error=20message=20=E2=80=94=20Trying=20to=20unit=20t?= =?UTF-8?q?est=20the=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projectMemberInvites/create.spec.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 4dabe90d..c2d638c5 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -316,6 +316,50 @@ describe('Project Member Invite create', () => { }); }); + it('should return 201 and empty response when trying add already invited member', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40051335], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.length.should.equal(0) + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.false; + done(); + } + }); + }); + it('should return 403 if try to create manager without MANAGER_ROLES', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ From 6558bb11445ea49da072bc0aa510814bd4c23eca Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Sat, 12 Jan 2019 11:11:50 +0530 Subject: [PATCH 39/50] lint fix --- src/routes/projectMemberInvites/create.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index c2d638c5..dbdcf5ef 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -353,7 +353,7 @@ describe('Project Member Invite create', () => { } else { const resJson = res.body.result.content; should.exist(resJson); - resJson.length.should.equal(0) + resJson.length.should.equal(0); server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.false; done(); } From 1dd16df7b039b64057c9c81168ac6eab7c0fcef8 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Sat, 12 Jan 2019 11:52:21 +0530 Subject: [PATCH 40/50] Fixed unit tests --- src/routes/projectMemberInvites/create.spec.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index dbdcf5ef..382469f9 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -81,6 +81,11 @@ describe('Project Member Invite create', () => { let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); + // restoring the stubs in beforeEach instead of afterEach because these methods are already stubbed + server.services.pubsub.init.restore(); + server.services.pubsub.publish.restore(); + sinon.stub(server.services.pubsub, 'init', () => {}); + sinon.stub(server.services.pubsub, 'publish', () => {}); }); afterEach(() => { sandbox.restore(); @@ -335,7 +340,7 @@ describe('Project Member Invite create', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post(`/v4/projects/${project2.id}/members/invite`) + .post(`/v4/projects/${project1.id}/members/invite`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) @@ -354,7 +359,7 @@ describe('Project Member Invite create', () => { const resJson = res.body.result.content; should.exist(resJson); resJson.length.should.equal(0); - server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.false; + server.services.pubsub.publish.neverCalledWith('project.member.invite.created').should.be.true; done(); } }); From 98a7a22d10486918af7b5c6c18c0847394dd6696 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 14 Jan 2019 18:56:35 +0800 Subject: [PATCH 41/50] fix connect app issue 2741 - [Team Management 2.0] Handle the case when email entered to be invited is already topcoder user --- src/routes/projectMemberInvites/create.js | 30 ++++++++++- .../projectMemberInvites/create.spec.js | 53 +++++++++++++++++++ src/util.js | 38 +++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 98c3eb2d..d9f92598 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -118,9 +118,35 @@ module.exports = [ data.userId = null; if (invite.emails) { + // if for some emails there are already existent users, we will invite them by userId, + // to avoid sending them registration email + const existentUsers = util.lookupUserEmails(req, invite.emails).map((user) => { + const userWithNumberId = {}; + _.assign(userWithNumberId, user, { + id: parseInt(user.id, 10), + }); + return userWithNumberId; + }); + // for existent users - invite by ids + const existentUserIds = _.map(existentUsers, 'id'); + // the rest of email of non-existent users, so we will invite them by email + const nonExistentUserEmails = invite.emails.filter(inviteEmail => + !_.find(existentUsers, { email: inviteEmail }), + ); + + // remove invites for users that are invited already + _.remove(existentUserIds, userId => _.some(invites, i => i.userId === userId)); + existentUserIds.forEach((userId) => { + const dataNew = _.clone(data); + _.assign(dataNew, { + userId, + }); + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + // remove invites for users that are invited already - _.remove(invite.emails, u => _.some(invites, i => i.email === u)); - invite.emails.forEach((email) => { + _.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email)); + nonExistentUserEmails.forEach((email) => { const dataNew = _.clone(data); _.assign(dataNew, { email, diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 382469f9..41ceb687 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -86,6 +86,8 @@ describe('Project Member Invite create', () => { server.services.pubsub.publish.restore(); sinon.stub(server.services.pubsub, 'init', () => {}); sinon.stub(server.services.pubsub, 'publish', () => {}); + // by default mock lookupUserEmails return nothing so all the cases are not broken + sandbox.stub(util, 'lookupUserEmails', () => []); }); afterEach(() => { sandbox.restore(); @@ -275,6 +277,57 @@ describe('Project Member Invite create', () => { }); }); + it('should return 201 and add new userId invite as customer for existent user when invite by email', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + util.lookupUserEmails.restore(); + sandbox.stub(util, 'lookupUserEmails', () => [{ + id: '12345', + email: 'hello@world.com', + }]); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['hello@world.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content[0]; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.projectId.should.equal(project2.id); + resJson.userId.should.equal(12345); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; + done(); + } + }); + }); + it('should return 201 and add new user invite as customer', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ diff --git a/src/util.js b/src/util.js index 3909fd95..a3f1fefb 100644 --- a/src/util.js +++ b/src/util.js @@ -423,6 +423,44 @@ _.assignIn(util, { return Promise.reject(err); }); }), + + /** + * Lookup user handles from emails + * @param {Object} req request + * @param {Array} userEmails user emails + * @param {Boolean} isPattern flag to indicate that pattern matching is required or not + * @return {Promise} promise + */ + lookupUserEmails: (req, userEmails, isPattern = false) => { + req.log.debug(`identityServiceEndpoint: ${config.get('identityServiceEndpoint')}`); + let filter = _.map(userEmails, i => `email=${i}`).join(' OR '); + if (isPattern) { + filter += '&like=true'; + } + req.log.trace('filter for users api call', filter); + return util.getSystemUserToken(req.log) + .then((token) => { + req.log.debug(`Bearer ${token}`); + const httpClient = this.getHttpClient({ id: req.id, log: req.log }); + return httpClient.get(`${config.get('identityServiceEndpoint')}users`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + params: { + fields: 'handle,id,email', + filter, + }, + }) + .then((response) => { + const data = _.get(response, 'data.result.content', null); + if (!data) { throw new Error('Response does not have result.content'); } + req.log.debug('UserHandle response', data); + return data; + }); + }); + }, }); export default util; From c52e099fa9c784bd4d10512f6d073976b201439b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 15 Jan 2019 10:08:10 +0800 Subject: [PATCH 42/50] improved unit test --- src/routes/projectMemberInvites/create.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 41ceb687..2f95e344 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -322,6 +322,7 @@ describe('Project Member Invite create', () => { resJson.role.should.equal('customer'); resJson.projectId.should.equal(project2.id); resJson.userId.should.equal(12345); + should.not.exist(resJson.email); server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; done(); } From fa2fd52fd0e1d0cfbc4aeaed91beb4fd75b5a07b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 15 Jan 2019 14:32:24 +0800 Subject: [PATCH 43/50] fix for inviting existent users --- src/routes/projectMemberInvites/create.js | 230 ++++++++++-------- .../projectMemberInvites/create.spec.js | 6 +- 2 files changed, 126 insertions(+), 110 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index d9f92598..7de0748b 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -28,6 +28,70 @@ const addMemberValidations = { }, }; +/** + * Helper method to build promises for creating new invites in DB + * + * @param {Object} req express request object + * @param {Object} invite invite to process + * @param {Array} invites existent invites from DB + * @param {Object} data template for new invites to be put in DB + * + * @returns {Promise} list of promises + */ +const buildCreateInvitePromises = (req, invite, invites, data) => { + const invitePromises = []; + + if (invite.userIds) { + // remove invites for users that are invited already + _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); + invite.userIds.forEach((userId) => { + const dataNew = _.clone(data); + + dataNew.userId = userId; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + + if (invite.emails) { + // if for some emails there are already existent users, we will invite them by userId, + // to avoid sending them registration email + return util.lookupUserEmails(req, invite.emails) + .then((existentUsers) => { + // for existent users - invite by ids + const existentUserIds = existentUsers.map(user => parseInt(user.id, 10)); + // the rest of email of non-existent users, so we will invite them by email + const nonExistentUserEmails = invite.emails.filter(inviteEmail => + !_.find(existentUsers, { email: inviteEmail }), + ); + + // remove invites for users that are invited already + _.remove(existentUserIds, userId => _.some(invites, i => i.userId === userId)); + existentUserIds.forEach((userId) => { + const dataNew = _.clone(data); + + dataNew.userId = userId; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + + // remove invites for users that are invited already + _.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email)); + nonExistentUserEmails.forEach((email) => { + const dataNew = _.clone(data); + + dataNew.email = email; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + + return Promise.resolve(invitePromises); + }); + } + + return Promise.resolve(invitePromises); +}; + module.exports = [ // handles request validations validate(addMemberValidations), @@ -103,120 +167,72 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, }; - const invitePromises = []; - if (invite.userIds) { - // remove invites for users that are invited already - _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); - invite.userIds.forEach((userId) => { - const dataNew = _.clone(data); - _.assign(dataNew, { - userId, - }); - invitePromises.push(models.ProjectMemberInvite.create(dataNew)); - }); - } - data.userId = null; - - if (invite.emails) { - // if for some emails there are already existent users, we will invite them by userId, - // to avoid sending them registration email - const existentUsers = util.lookupUserEmails(req, invite.emails).map((user) => { - const userWithNumberId = {}; - _.assign(userWithNumberId, user, { - id: parseInt(user.id, 10), - }); - return userWithNumberId; - }); - // for existent users - invite by ids - const existentUserIds = _.map(existentUsers, 'id'); - // the rest of email of non-existent users, so we will invite them by email - const nonExistentUserEmails = invite.emails.filter(inviteEmail => - !_.find(existentUsers, { email: inviteEmail }), - ); - - // remove invites for users that are invited already - _.remove(existentUserIds, userId => _.some(invites, i => i.userId === userId)); - existentUserIds.forEach((userId) => { - const dataNew = _.clone(data); - _.assign(dataNew, { - userId, - }); - invitePromises.push(models.ProjectMemberInvite.create(dataNew)); - }); - - // remove invites for users that are invited already - _.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email)); - nonExistentUserEmails.forEach((email) => { - const dataNew = _.clone(data); - _.assign(dataNew, { - email, - }); - invitePromises.push(models.ProjectMemberInvite.create(dataNew)); - }); - } - if (invitePromises.length === 0) { - return []; - } - - req.log.debug('Creating invites'); - const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; - return models.sequelize.Promise.all(invitePromises) - .then((values) => { - values.forEach((v) => { - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, { - req, - userId: v.userId, - email: v.email, - }); - req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, - v, - { correlationId: req.id }, - ); - // send email invite (async) - if (v.email) { - models.Project - .find({ - where: { id: projectId }, - raw: true, - }) - .then((_project) => { - createEvent(emailEventType, - { - data: { - connectURL: config.get('connectUrl'), - accountsAppURL: config.get('accountsAppUrl'), - subject: config.get('inviteEmailSubject'), - projects: [ - { - name: _project.name, - projectId, - sections: [ + return buildCreateInvitePromises(req, invite, invites, data) + .then((invitePromises) => { + if (invitePromises.length === 0) { + return []; + } + + req.log.debug('Creating invites'); + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; + return models.sequelize.Promise.all(invitePromises) + .then((values) => { + values.forEach((v) => { + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, { + req, + userId: v.userId, + email: v.email, + }); + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, + v, + { correlationId: req.id }, + ); + // send email invite (async) + if (v.email) { + models.Project + .find({ + where: { id: projectId }, + raw: true, + }) + .then((_project) => { + createEvent(emailEventType, + { + data: { + connectURL: config.get('connectUrl'), + accountsAppURL: config.get('accountsAppUrl'), + subject: config.get('inviteEmailSubject'), + projects: [ { - EMAIL_INVITES: true, - title: config.get('inviteEmailSectionTitle'), - projectName: _project.name, + name: _project.name, projectId, + sections: [ + { + EMAIL_INVITES: true, + title: config.get('inviteEmailSectionTitle'), + projectName: _project.name, + projectId, + }, + ], }, ], }, - ], - }, - recipients: [v.email], - version: 'v3', - from: { - name: config.get('EMAIL_INVITE_FROM_NAME'), - email: config.get('EMAIL_INVITE_FROM_EMAIL'), - }, - categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], - }, req.log); + recipients: [v.email], + version: 'v3', + from: { + name: config.get('EMAIL_INVITE_FROM_NAME'), + email: config.get('EMAIL_INVITE_FROM_EMAIL'), + }, + categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], + }, req.log); + }); + } }); - } - }); - return values; - }); - }); + return values; + }); // models.sequelize.Promise.all + }); // buildCreateInvitePromises + }); // models.ProjectMemberInvite.getPendingInvitesForProject }) .then(values => res.status(201).json(util.wrapResponse(req.id, values, null, 201))) .catch(err => next(err)); diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 2f95e344..6b318e5d 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -87,7 +87,7 @@ describe('Project Member Invite create', () => { sinon.stub(server.services.pubsub, 'init', () => {}); sinon.stub(server.services.pubsub, 'publish', () => {}); // by default mock lookupUserEmails return nothing so all the cases are not broken - sandbox.stub(util, 'lookupUserEmails', () => []); + sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([])); }); afterEach(() => { sandbox.restore(); @@ -296,10 +296,10 @@ describe('Project Member Invite create', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); util.lookupUserEmails.restore(); - sandbox.stub(util, 'lookupUserEmails', () => [{ + sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([{ id: '12345', email: 'hello@world.com', - }]); + }])); request(server) .post(`/v4/projects/${project2.id}/members/invite`) .set({ From abe921a4ee29a7930ebb840ec93480f5f1bc5da4 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 15 Jan 2019 15:03:13 +0800 Subject: [PATCH 44/50] fix 2 for inviting existent users --- src/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index a3f1fefb..c1386281 100644 --- a/src/util.js +++ b/src/util.js @@ -441,7 +441,7 @@ _.assignIn(util, { return util.getSystemUserToken(req.log) .then((token) => { req.log.debug(`Bearer ${token}`); - const httpClient = this.getHttpClient({ id: req.id, log: req.log }); + const httpClient = util.getHttpClient({ id: req.id, log: req.log }); return httpClient.get(`${config.get('identityServiceEndpoint')}users`, { headers: { Authorization: `Bearer ${token}`, From ba99512ea76ca9eb7aad5c5071a2cca1795bfc89 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 15 Jan 2019 15:43:39 +0800 Subject: [PATCH 45/50] fix 3 for inviting existent users --- src/routes/projectMemberInvites/create.js | 21 ++++++++++++------- .../projectMemberInvites/create.spec.js | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 7de0748b..a0ac4a63 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -58,19 +58,26 @@ const buildCreateInvitePromises = (req, invite, invites, data) => { // to avoid sending them registration email return util.lookupUserEmails(req, invite.emails) .then((existentUsers) => { - // for existent users - invite by ids - const existentUserIds = existentUsers.map(user => parseInt(user.id, 10)); - // the rest of email of non-existent users, so we will invite them by email + // existent user we will invite by userId and email + const existentUsersWithNumberId = existentUsers.map((user) => { + const userWithNumberId = _.clone(user); + + userWithNumberId.id = parseInt(user.id, 10); + + return userWithNumberId; + }); + // non-existent users we will invite them by email only const nonExistentUserEmails = invite.emails.filter(inviteEmail => !_.find(existentUsers, { email: inviteEmail }), ); // remove invites for users that are invited already - _.remove(existentUserIds, userId => _.some(invites, i => i.userId === userId)); - existentUserIds.forEach((userId) => { + _.remove(existentUsersWithNumberId, user => _.some(invites, i => i.userId === user.id)); + existentUsersWithNumberId.forEach((user) => { const dataNew = _.clone(data); - dataNew.userId = userId; + dataNew.userId = user.id; + dataNew.email = user.email; invitePromises.push(models.ProjectMemberInvite.create(dataNew)); }); @@ -190,7 +197,7 @@ module.exports = [ { correlationId: req.id }, ); // send email invite (async) - if (v.email) { + if (v.email && !v.userId) { models.Project .find({ where: { id: projectId }, diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 6b318e5d..59541d8e 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -322,7 +322,7 @@ describe('Project Member Invite create', () => { resJson.role.should.equal('customer'); resJson.projectId.should.equal(project2.id); resJson.userId.should.equal(12345); - should.not.exist(resJson.email); + resJson.email.should.equal('hello@world.com'); server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; done(); } From 2766b8eb0d5ce74f9cd19d92abbd28fb83562581 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 15 Jan 2019 13:17:58 +0530 Subject: [PATCH 46/50] trying to send initiator user details in email data --- src/routes/projectMemberInvites/create.js | 84 +++++++++++++---------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index a0ac4a63..e2e1dfcc 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -99,6 +99,52 @@ const buildCreateInvitePromises = (req, invite, invites, data) => { return Promise.resolve(invitePromises); }; +const sendInviteEmail = (req, invite) => { + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; + const promises = [ + models.Project.find({ + where: { id: projectId }, + raw: true, + }), + util.getMemberDetailsByUserIds(req.authUser.userId, req.logger, req.id), + ]; + return Promise.all(promises).then((responses) => { + const _project = responses[0]; + const initiator = responses[1] && responses[1].length ? responses[1][0] : null; + createEvent(emailEventType, { + data: { + connectURL: config.get('connectUrl'), + accountsAppURL: config.get('accountsAppUrl'), + subject: config.get('inviteEmailSubject'), + projects: [{ + name: _project.name, + projectId, + sections: [ + { + EMAIL_INVITES: true, + title: config.get('inviteEmailSectionTitle'), + projectName: _project.name, + projectId, + initiator: initiator ? initiator : { + userId: req.authUser.userId, + firstName: 'Connect', + lastName: 'User', + } + }, + ], + }], + }, + recipients: [invite.email], + version: 'v3', + from: { + name: config.get('EMAIL_INVITE_FROM_NAME'), + email: config.get('EMAIL_INVITE_FROM_EMAIL'), + }, + categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], + }, req.log); + }); +} + module.exports = [ // handles request validations validate(addMemberValidations), @@ -182,7 +228,6 @@ module.exports = [ } req.log.debug('Creating invites'); - const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; return models.sequelize.Promise.all(invitePromises) .then((values) => { values.forEach((v) => { @@ -198,42 +243,7 @@ module.exports = [ ); // send email invite (async) if (v.email && !v.userId) { - models.Project - .find({ - where: { id: projectId }, - raw: true, - }) - .then((_project) => { - createEvent(emailEventType, - { - data: { - connectURL: config.get('connectUrl'), - accountsAppURL: config.get('accountsAppUrl'), - subject: config.get('inviteEmailSubject'), - projects: [ - { - name: _project.name, - projectId, - sections: [ - { - EMAIL_INVITES: true, - title: config.get('inviteEmailSectionTitle'), - projectName: _project.name, - projectId, - }, - ], - }, - ], - }, - recipients: [v.email], - version: 'v3', - from: { - name: config.get('EMAIL_INVITE_FROM_NAME'), - email: config.get('EMAIL_INVITE_FROM_EMAIL'), - }, - categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], - }, req.log); - }); + sendInviteEmail(req, v); } }); return values; From a40bc202ee4ffa7e17c0a6be14c243b1f4fa4a45 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 15 Jan 2019 14:06:37 +0530 Subject: [PATCH 47/50] lint fix --- src/routes/projectMemberInvites/create.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index e2e1dfcc..21ebd752 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -99,7 +99,7 @@ const buildCreateInvitePromises = (req, invite, invites, data) => { return Promise.resolve(invitePromises); }; -const sendInviteEmail = (req, invite) => { +const sendInviteEmail = (req, projectId, invite) => { const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; const promises = [ models.Project.find({ @@ -109,27 +109,27 @@ const sendInviteEmail = (req, invite) => { util.getMemberDetailsByUserIds(req.authUser.userId, req.logger, req.id), ]; return Promise.all(promises).then((responses) => { - const _project = responses[0]; - const initiator = responses[1] && responses[1].length ? responses[1][0] : null; + const project = responses[0]; + const initiator = responses[1] && responses[1].length ? responses[1][0] : { + userId: req.authUser.userId, + firstName: 'Connect', + lastName: 'User', + }; createEvent(emailEventType, { data: { connectURL: config.get('connectUrl'), accountsAppURL: config.get('accountsAppUrl'), subject: config.get('inviteEmailSubject'), projects: [{ - name: _project.name, + name: project.name, projectId, sections: [ { EMAIL_INVITES: true, title: config.get('inviteEmailSectionTitle'), - projectName: _project.name, + projectName: project.name, projectId, - initiator: initiator ? initiator : { - userId: req.authUser.userId, - firstName: 'Connect', - lastName: 'User', - } + initiator, }, ], }], @@ -143,7 +143,7 @@ const sendInviteEmail = (req, invite) => { categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], }, req.log); }); -} +}; module.exports = [ // handles request validations From 04ed8977a0a75ad13c0f2fb69e6295d6ca244827 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 15 Jan 2019 14:46:58 +0530 Subject: [PATCH 48/50] Fixed unit tests --- src/routes/projectMemberInvites/create.js | 9 +++++++-- src/routes/projectMemberInvites/create.spec.js | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 21ebd752..d2087d35 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -93,6 +93,9 @@ const buildCreateInvitePromises = (req, invite, invites, data) => { }); return Promise.resolve(invitePromises); + }).catch((error) => { + req.log.error(error); + return Promise.reject(invitePromises); }); } @@ -106,7 +109,7 @@ const sendInviteEmail = (req, projectId, invite) => { where: { id: projectId }, raw: true, }), - util.getMemberDetailsByUserIds(req.authUser.userId, req.logger, req.id), + util.getMemberDetailsByUserIds(req.authUser.userId, req.log, req.id), ]; return Promise.all(promises).then((responses) => { const project = responses[0]; @@ -142,6 +145,8 @@ const sendInviteEmail = (req, projectId, invite) => { }, categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], }, req.log); + }).catch((error) => { + req.log.error(error); }); }; @@ -243,7 +248,7 @@ module.exports = [ ); // send email invite (async) if (v.email && !v.userId) { - sendInviteEmail(req, v); + sendInviteEmail(req, projectId, v); } }); return values; diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 59541d8e..e648a06f 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -88,6 +88,11 @@ describe('Project Member Invite create', () => { sinon.stub(server.services.pubsub, 'publish', () => {}); // by default mock lookupUserEmails return nothing so all the cases are not broken sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([])); + sandbox.stub(util, 'getMemberDetailsByUserIds', () => Promise.resolve([{ + userId: 40051333, + firstName: 'Admin', + lastName: 'User', + }])); }); afterEach(() => { sandbox.restore(); From 50c73aa5eb857d96a6bcd33699a0f69c3607cab9 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 15 Jan 2019 17:29:20 +0800 Subject: [PATCH 49/50] setting longer timeout (5000 instead of 3000) for lookupUserEmails --- src/util.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util.js b/src/util.js index c1386281..ea20e3be 100644 --- a/src/util.js +++ b/src/util.js @@ -452,6 +452,8 @@ _.assignIn(util, { fields: 'handle,id,email', filter, }, + // set longer timeout as default 3000 could be not enough for identity service response + timeout: 5000, }) .then((response) => { const data = _.get(response, 'data.result.content', null); From 17b0bce744c42c2ddbd5a24eb55c37580d262769 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Tue, 15 Jan 2019 15:12:08 +0530 Subject: [PATCH 50/50] fixed syntax error --- src/routes/projectMemberInvites/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index d2087d35..490bbba6 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -109,7 +109,7 @@ const sendInviteEmail = (req, projectId, invite) => { where: { id: projectId }, raw: true, }), - util.getMemberDetailsByUserIds(req.authUser.userId, req.log, req.id), + util.getMemberDetailsByUserIds([req.authUser.userId], req.log, req.id), ]; return Promise.all(promises).then((responses) => { const project = responses[0];