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/local/mock-services/server.js b/local/mock-services/server.js index 029df5b7..c1f0069f 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 { @@ -38,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', @@ -53,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); @@ -60,6 +73,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..d2aafe30 --- /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 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 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..64b994cf 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,11 @@ 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', + PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.action.project.member.invite.created', }; export const REGEX = { @@ -141,3 +152,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..e25eb7a2 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,14 @@ 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 + // 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)]); logger.debug('elasticsearch index updated successfully and co-pilot/manager updated in direct project'); @@ -205,4 +209,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..ac33ee2d --- /dev/null +++ b/src/models/projectMemberInvite.js @@ -0,0 +1,90 @@ + +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 = { projectId, status: INVITE_STATUS.PENDING }; + + if (email && userId) { + _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] }); + } else 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 && userId) { + _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] }); + } else 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..6619b44a --- /dev/null +++ b/src/routes/projectMemberInvites/create.js @@ -0,0 +1,177 @@ + + +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, PROJECT_MEMBER_EMAIL_INVITE_CREATED } from '../../constants'; +import { createEvent } from '../../services/busApi'; + + +/** + * 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) { + // 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 + if (invite.role === PROJECT_MEMBER_ROLE.MANAGER) { + _.forEach(invite.userIds, (userId) => { + req.log.info(userId); + promises.push(util.getUserRoles(userId, req.log, req.id)); + }); + } + } + + 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) => { + 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)); + }); + } + + 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 }, + ); + // 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)); + }); + }); + }); + }).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..4dabe90d --- /dev/null +++ b/src/routes/projectMemberInvites/create.spec.js @@ -0,0 +1,529 @@ +/* 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; + 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(() => { + 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(201) + .end((err, res) => { + if (err) { + done(err); + } else { + res.body.result.status.should.equal(201); + 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 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(); + } + }); + }); + + 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.calledTwice.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..8de0fd5a --- /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, 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}, email ${req.authUser.email}`); + 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..300ba0ad --- /dev/null +++ b/src/routes/projectMemberInvites/update.js @@ -0,0 +1,120 @@ +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, 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); + + // 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.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) || + (!!putInvite.email && putInvite.email !== req.authUser.email)) { + 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: req.authUser.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..a137e57e --- /dev/null +++ b/src/routes/projectMemberInvites/update.spec.js @@ -0,0 +1,320 @@ +/* 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 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, /.*Project members can cancel invites only for customer/); + 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: 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, /.*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/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/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)); 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..1e490f57 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,26 @@ 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) => { + const allowedProjectIds = accessibleProjectIds; + // get projects with pending invite for current user + const invites = models.ProjectMemberInvite.getProjectInvitesForUser( + req.authUser.email, + req.authUser.userId); + + return invites.then((ids => _.union(allowedProjectIds, ids))); + }) + .then((allowedProjectIds) => { // 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" +