diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b315cea..459ec723 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ workflows: - test filters: branches: - only: ['dev', 'feature/auth0-proxy-server'] + only: ['dev'] - deployProd: requires: - test diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 6620f43c..c807e407 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -47,5 +47,9 @@ "AUTH0_AUDIENCE": "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME" : "TOKEN_CACHE_TIME", "whitelistedOriginsForUserIdAuth": "WHITELISTED_ORIGINS_FOR_USERID_AUTH", - "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL" + "AUTH0_PROXY_SERVER_URL" : "AUTH0_PROXY_SERVER_URL", + "connectUrl": "CONNECT_URL", + "accountsAppUrl": "ACCOUNTS_APP_URL", + "inviteEmailSubject": "INVITE_EMAIL_SUBJECT", + "inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE" } diff --git a/config/default.json b/config/default.json index a23a86e8..2b0ac247 100644 --- a/config/default.json +++ b/config/default.json @@ -51,5 +51,12 @@ "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", + "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", + "inviteEmailSubject": "You are invited to Topcoder", + "inviteEmailSectionTitle": "Project Invitation", + "connectUrl":"https://connect.topcoder-dev.com", + "accountsAppUrl": "https://accounts.topcoder-dev.com" + } diff --git a/deploy.sh b/deploy.sh index 059b8165..187c5816 100755 --- a/deploy.sh +++ b/deploy.sh @@ -152,6 +152,14 @@ make_task_def(){ "name": "CONNECT_PROJECTS_URL", "value": "%s" }, + { + "name": "CONNECT_URL", + "value": "%s" + }, + { + "name": "ACCOUNTS_APP_URL", + "value": "%s" + }, { "name": "SEGMENT_ANALYTICS_KEY", "value": "%s" @@ -195,6 +203,22 @@ make_task_def(){ { "name": "AUTH0_PROXY_SERVER_URL", "value": "%s" + }, + { + "name": "EMAIL_INVITE_FROM_NAME", + "value": "%s" + }, + { + "name": "EMAIL_INVITE_FROM_EMAIL", + "value": "%s" + }, + { + "name": "INVITE_EMAIL_SUBJECT", + "value": "%s" + }, + { + "name": "INVITE_EMAIL_SECTION_TITLE", + "value": "%s" } ], "portMappings": [ @@ -230,6 +254,8 @@ make_task_def(){ DIRECT_PROJECT_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_DIRECT_PROJECT_SERVICE_ENDPOINT") FILE_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_FILE_SERVICE_ENDPOINT") CONNECT_PROJECTS_URL=$(eval "echo \$${ENV}_CONNECT_PROJECTS_URL") + CONNECT_URL=$(eval "echo \$${ENV}_CONNECT_URL") + ACCOUNTS_APP_URL=$(eval "echo \$${ENV}_ACCOUNTS_APP_URL") SEGMENT_ANALYTICS_KEY=$(eval "echo \$${ENV}_SEGMENT_ANALYTICS_KEY") MESSAGE_SERVICE_URL=$(eval "echo \$${ENV}_MESSAGE_SERVICE_URL") if [ "$ENV" = "PROD" ]; then @@ -250,11 +276,14 @@ make_task_def(){ KAFKA_CLIENT_CERT_KEY=$(eval "echo \$${ENV}_KAFKA_CLIENT_CERT_KEY") KAFKA_GROUP_ID=$(eval "echo \$${ENV}_KAFKA_GROUP_ID") KAFKA_URL=$(eval "echo \$${ENV}_KAFKA_URL") - AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") AUTH0_PROXY_SERVER_URL=$(eval "echo \$${ENV}_AUTH0_PROXY_SERVER_URL") + EMAIL_INVITE_FROM_NAME=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_NAME") + EMAIL_INVITE_FROM_EMAIL=$(eval "echo \$${ENV}_EMAIL_INVITE_FROM_EMAIL") + INVITE_EMAIL_SUBJECT=$(eval "echo \$${ENV}_INVITE_EMAIL_SUBJECT") + INVITE_EMAIL_SECTION_TITLE=$(eval "echo \$${ENV}_INVITE_EMAIL_SECTION_TITLE") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" "$INVITE_EMAIL_SUBJECT" "$INVITE_EMAIL_SECTION_TITLE" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ diff --git a/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..64b350f8 --- /dev/null +++ b/migrations/20181201_create_project_member_invites.sql @@ -0,0 +1,41 @@ +-- +-- 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 +); + +ALTER TABLE ONLY project_member_invites + ADD CONSTRAINT project_member_invites_pkey PRIMARY KEY (id); + +CREATE SEQUENCE project_member_invites_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE project_member_invites_id_seq OWNED BY project_member_invites.id; + +ALTER TABLE project_member_invites + ALTER COLUMN id SET DEFAULT nextval('project_member_invites_id_seq'); + +ALTER TABLE ONLY project_member_invites + ADD CONSTRAINT "project_member_invites_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES projects(id) ON UPDATE CASCADE ON DELETE CASCADE; \ No newline at end of file diff --git a/migrations/20181226_productTemplates_subCategory.sql b/migrations/20181226_productTemplates_subCategory.sql new file mode 100644 index 00000000..8626b0e2 --- /dev/null +++ b/migrations/20181226_productTemplates_subCategory.sql @@ -0,0 +1,14 @@ +-- +-- UPDATE EXISTING TABLES: +-- product_templates: +-- added column `subCategory` + +-- +-- product_templates + +-- Add new column +ALTER TABLE product_templates ADD COLUMN "subCategory" character varying(45); +-- Update new column +UPDATE product_templates SET "subCategory"="category" WHERE "subCategory" is NULL; +-- Set not null +ALTER TABLE product_templates ALTER COLUMN "subCategory" SET NOT NULL; diff --git a/package-lock.json b/package-lock.json index 228a50d6..20e07864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", "requires": { "@types/connect": "3.4.32", - "@types/node": "10.9.4" + "@types/node": "10.12.15" } }, "@types/connect": { @@ -32,12 +32,12 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", "requires": { - "@types/node": "10.9.4" + "@types/node": "10.12.15" } }, "@types/events": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, "@types/express": { @@ -65,8 +65,8 @@ "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", "requires": { "@types/events": "1.2.0", - "@types/node": "10.9.4", - "@types/range-parser": "1.2.2" + "@types/node": "10.12.15", + "@types/range-parser": "1.2.3" } }, "@types/express-unless": { @@ -93,14 +93,14 @@ "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" }, "@types/node": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", - "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" + "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.2", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", - "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" + "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", @@ -422,23 +422,23 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "auth0-js": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.7.3.tgz", - "integrity": "sha512-iZAqoN4EbsNCS/3VkFPNb4glTyj8hq57T7gcUF+XH8Rua7hBTUzpb101K9zqcdUIBilIdF9XBLCTJ4JGgZ/oFA==", + "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": "1.1.0", - "winchan": "0.2.0" + "url-join": "4.0.0", + "winchan": "0.2.1" }, "dependencies": { "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "2.1.1" } @@ -479,7 +479,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "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" @@ -492,7 +492,7 @@ "requires": { "component-emitter": "1.2.1", "cookiejar": "2.1.1", - "debug": "3.2.5", + "debug": "3.2.6", "extend": "3.0.1", "form-data": "2.3.1", "formidable": "1.2.1", @@ -1710,7 +1710,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, "requires": { "tweetnacl": "0.14.5" } @@ -2036,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", @@ -2139,7 +2137,7 @@ "dependencies": { "semver": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.1.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.0.1.tgz", "integrity": "sha1-n7P0AE+QDYPEeWj+QvdYPgWDLMk=" } } @@ -2187,6 +2185,64 @@ "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz", "integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=" }, + "compressible": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz", + "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==", + "requires": { + "mime-db": "1.37.0" + }, + "dependencies": { + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + } + } + }, + "compression": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", + "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", + "requires": { + "accepts": "1.3.5", + "bytes": "3.0.0", + "compressible": "2.0.15", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.2", + "vary": "1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.21", + "negotiator": "0.6.1" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "1.37.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2620,7 +2676,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, "requires": { "jsbn": "0.1.1", "safer-buffer": "2.1.2" @@ -3217,9 +3272,9 @@ } }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "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", @@ -3501,542 +3556,6 @@ "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-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" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.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 - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -4490,23 +4009,23 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "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": "5.5.2", + "ajv": "6.6.1", "har-schema": "2.0.0" }, "dependencies": { "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", + "fast-deep-equal": "2.0.1", "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" } } } @@ -4583,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", @@ -4608,7 +4124,7 @@ "requires": { "assert-plus": "1.0.0", "jsprim": "1.4.1", - "sshpk": "1.14.2" + "sshpk": "1.15.2" } }, "iconv-lite": { @@ -4629,9 +4145,9 @@ }, "dependencies": { "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "2.1.1" } @@ -4672,7 +4188,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "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" @@ -4685,7 +4201,7 @@ "requires": { "component-emitter": "1.2.1", "cookiejar": "2.1.1", - "debug": "3.2.5", + "debug": "3.2.6", "extend": "3.0.1", "form-data": "2.3.1", "formidable": "1.2.1", @@ -4694,6 +4210,11 @@ "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=" } } }, @@ -5354,9 +4875,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "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", @@ -5505,26 +5026,26 @@ } }, "le_node": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.7.1.tgz", - "integrity": "sha1-gxZAna2oK58pZXykBgZj+PEVUdE=", + "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": "3.9.3", + "lodash": "4.17.11", "reconnect-core": "1.3.0", "semver": "5.1.0" }, "dependencies": { "lodash": { - "version": "3.9.3", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz", - "integrity": "sha1-AVnoaDL+/8bWHYUrEqlTuZSWvTI=" + "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": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.1.0.tgz", "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" } } @@ -5901,7 +5422,7 @@ "dependencies": { "lru-cache": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", "requires": { "pseudomap": "1.0.2", @@ -6404,6 +5925,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6878,9 +6404,9 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "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", @@ -7248,16 +6774,16 @@ "aws-sign2": "0.7.0", "aws4": "1.8.0", "caseless": "0.12.0", - "combined-stream": "1.0.6", + "combined-stream": "1.0.7", "extend": "3.0.2", "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.1.0", + "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.20", + "mime-types": "2.1.21", "oauth-sign": "0.9.0", "performance-now": "2.1.0", "qs": "6.5.2", @@ -7268,9 +6794,9 @@ }, "dependencies": { "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "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" } @@ -7281,26 +6807,26 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "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.6", - "mime-types": "2.1.20" + "combined-stream": "1.0.7", + "mime-types": "2.1.21" } }, "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + "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.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "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.36.0" + "mime-db": "1.37.0" } }, "qs": { @@ -7817,9 +7343,9 @@ "dev": true }, "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "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", @@ -8076,16 +7602,15 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", + "version": "github:appirio-tech/tc-core-library-js#02350d46d3b8d89ee4686d5c1a5d0086943cbfe8", "requires": { - "auth0-js": "9.7.3", + "auth0-js": "9.8.2", "axios": "0.12.0", "bunyan": "1.8.12", - "config": "1.27.0", - "jsonwebtoken": "7.4.3", + "jsonwebtoken": "8.3.0", "jwks-rsa": "1.3.0", - "le_node": "1.7.1", - "lodash": "4.17.4", + "le_node": "1.8.0", + "lodash": "4.17.11", "millisecond": "0.1.2" }, "dependencies": { @@ -8106,46 +7631,10 @@ "stream-consume": "0.1.0" } }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "isemail": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", - "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" - }, - "joi": { - "version": "6.10.1", - "resolved": "http://registry.npmjs.org/joi/-/joi-6.10.1.tgz", - "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", - "requires": { - "hoek": "2.16.3", - "isemail": "1.2.0", - "moment": "2.22.2", - "topo": "1.1.0" - } - }, - "jsonwebtoken": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", - "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", - "requires": { - "joi": "6.10.1", - "jws": "3.1.5", - "lodash.once": "4.1.1", - "ms": "2.0.0", - "xtend": "4.0.1" - } - }, - "topo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", - "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", - "requires": { - "hoek": "2.16.3" - } + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" } } }, @@ -8308,7 +7797,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { - "psl": "1.1.29", + "psl": "1.1.31", "punycode": "1.4.1" }, "dependencies": { @@ -8347,8 +7836,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", @@ -8510,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", @@ -8520,9 +8023,9 @@ } }, "url-join": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", - "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + "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", @@ -8742,9 +8245,9 @@ } }, "winchan": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.0.tgz", - "integrity": "sha1-OGMCjn+XSw2hQS8oQXukJJcqvZQ=" + "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", 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..032e1e8d 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.email.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..94edcff3 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -658,4 +658,31 @@ 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, + initiatorUserId: req.authUser.userId, + }, 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, + initiatorUserId: req.authUser.userId, + }, 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/productTemplate.js b/src/models/productTemplate.js index 4e4dc184..f8fd7f17 100644 --- a/src/models/productTemplate.js +++ b/src/models/productTemplate.js @@ -9,6 +9,7 @@ module.exports = (sequelize, DataTypes) => { name: { type: DataTypes.STRING(255), allowNull: false }, productKey: { type: DataTypes.STRING(45), allowNull: false }, category: { type: DataTypes.STRING(45), allowNull: false }, + subCategory: { type: DataTypes.STRING(45), allowNull: false }, icon: { type: DataTypes.STRING(255), allowNull: false }, brief: { type: DataTypes.STRING(45), allowNull: false }, details: { type: DataTypes.STRING(255), allowNull: false }, diff --git a/src/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 469c2c2f..dabe7454 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -60,10 +60,12 @@ router.all( ); // Register all the routes +router.use('/v4/projects', compression()); router.route('/v4/projects') .post(require('./projects/create')) .get(require('./projects/list')); +router.use('/v4/projects/db', compression()); router.route('/v4/projects/db') .get(require('./projects/list-db')); @@ -175,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/metadata/list.spec.js b/src/routes/metadata/list.spec.js index 7e7c9b4f..86798d21 100644 --- a/src/routes/metadata/list.spec.js +++ b/src/routes/metadata/list.spec.js @@ -30,6 +30,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/milestoneTemplates/clone.spec.js b/src/routes/milestoneTemplates/clone.spec.js index 03ad322a..a0f9bbbd 100644 --- a/src/routes/milestoneTemplates/clone.spec.js +++ b/src/routes/milestoneTemplates/clone.spec.js @@ -14,6 +14,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -47,6 +48,7 @@ const productTemplates = [ name: 'name 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/create.spec.js b/src/routes/milestoneTemplates/create.spec.js index 00dad710..beacf71f 100644 --- a/src/routes/milestoneTemplates/create.spec.js +++ b/src/routes/milestoneTemplates/create.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/delete.spec.js b/src/routes/milestoneTemplates/delete.spec.js index 0d0d5527..92a6dcb6 100644 --- a/src/routes/milestoneTemplates/delete.spec.js +++ b/src/routes/milestoneTemplates/delete.spec.js @@ -40,6 +40,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -73,6 +74,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/get.spec.js b/src/routes/milestoneTemplates/get.spec.js index 58ce6a5a..50f31370 100644 --- a/src/routes/milestoneTemplates/get.spec.js +++ b/src/routes/milestoneTemplates/get.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/list.spec.js b/src/routes/milestoneTemplates/list.spec.js index 2ee2f25f..465a388f 100644 --- a/src/routes/milestoneTemplates/list.spec.js +++ b/src/routes/milestoneTemplates/list.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/milestoneTemplates/update.spec.js b/src/routes/milestoneTemplates/update.spec.js index 68be6f31..3de8c430 100644 --- a/src/routes/milestoneTemplates/update.spec.js +++ b/src/routes/milestoneTemplates/update.spec.js @@ -15,6 +15,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -48,6 +49,7 @@ const productTemplates = [ name: 'template 2', productKey: 'productKey 2', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 8bdcb5d8..69f45a4d 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -97,6 +97,7 @@ describe('Project Phases', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/create.js b/src/routes/productTemplates/create.js index 8f47ec4f..9907d085 100644 --- a/src/routes/productTemplates/create.js +++ b/src/routes/productTemplates/create.js @@ -16,6 +16,7 @@ const schema = { param: Joi.object().keys({ id: Joi.any().strip(), category: Joi.string().max(45).required(), + subCategory: Joi.string().max(45).required(), name: Joi.string().max(255).required(), productKey: Joi.string().max(45).required(), icon: Joi.string().max(255).required(), diff --git a/src/routes/productTemplates/create.spec.js b/src/routes/productTemplates/create.spec.js index b4da9239..0c283caf 100644 --- a/src/routes/productTemplates/create.spec.js +++ b/src/routes/productTemplates/create.spec.js @@ -35,6 +35,7 @@ describe('CREATE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/delete.spec.js b/src/routes/productTemplates/delete.spec.js index edb53d74..5c4d177c 100644 --- a/src/routes/productTemplates/delete.spec.js +++ b/src/routes/productTemplates/delete.spec.js @@ -43,6 +43,7 @@ describe('DELETE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/get.spec.js b/src/routes/productTemplates/get.spec.js index 42b34e30..fd7cd7da 100644 --- a/src/routes/productTemplates/get.spec.js +++ b/src/routes/productTemplates/get.spec.js @@ -15,6 +15,7 @@ describe('GET product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js index 9282e409..a9386f66 100644 --- a/src/routes/productTemplates/list.spec.js +++ b/src/routes/productTemplates/list.spec.js @@ -14,12 +14,13 @@ import testUtil from '../../tests/util'; const validateProductTemplates = (count, resJson, expectedTemplates) => { resJson.should.have.length(count); resJson.forEach((pt, idx) => { - pt.should.have.all.keys('id', 'name', 'productKey', 'category', 'icon', 'brief', 'details', 'aliases', - 'template', 'disabled', 'hidden', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); + pt.should.have.all.keys('id', 'name', 'productKey', 'category', 'subCategory', 'icon', 'brief', 'details', + 'aliases', 'template', 'disabled', 'hidden', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); pt.should.not.have.all.keys('deletedAt', 'deletedBy'); pt.name.should.be.eql(expectedTemplates[idx].name); pt.productKey.should.be.eql(expectedTemplates[idx].productKey); pt.category.should.be.eql(expectedTemplates[idx].category); + pt.subCategory.should.be.eql(expectedTemplates[idx].subCategory); pt.icon.should.be.eql(expectedTemplates[idx].icon); pt.brief.should.be.eql(expectedTemplates[idx].brief); pt.details.should.be.eql(expectedTemplates[idx].details); @@ -38,6 +39,7 @@ describe('LIST product templates', () => { name: 'name 1', productKey: 'productKey-1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', @@ -73,6 +75,7 @@ describe('LIST product templates', () => { name: 'template 2', productKey: 'productKey-2', category: 'concrete', + subCategory: 'concrete', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', diff --git a/src/routes/productTemplates/update.js b/src/routes/productTemplates/update.js index 82095299..39456144 100644 --- a/src/routes/productTemplates/update.js +++ b/src/routes/productTemplates/update.js @@ -21,10 +21,11 @@ const schema = { name: Joi.string().max(255), productKey: Joi.string().max(45), category: Joi.string().max(45), + subCategory: Joi.string().max(45), icon: Joi.string().max(255), brief: Joi.string().max(45), details: Joi.string().max(255), - aliases: Joi.object(), + aliases: Joi.array(), template: Joi.object(), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), diff --git a/src/routes/productTemplates/update.spec.js b/src/routes/productTemplates/update.spec.js index 6e65461d..f0223d4b 100644 --- a/src/routes/productTemplates/update.spec.js +++ b/src/routes/productTemplates/update.spec.js @@ -15,16 +15,11 @@ describe('UPDATE product template', () => { name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', - aliases: { - alias1: { - subAlias1A: 1, - subAlias1B: 2, - }, - alias2: [1, 2, 3], - }, + aliases: ['productTemplate-1', 'productTemplate_1'], disabled: true, hidden: true, template: { @@ -86,17 +81,11 @@ describe('UPDATE product template', () => { name: 'template 1 - update', productKey: 'productKey 1 - update', category: 'concrete', + subCategory: 'concrete', icon: 'http://example.com/icon1-update.ico', brief: 'brief 1 - update', details: 'details 1 - update', - aliases: { - alias1: { - subAlias1A: 11, - subAlias1C: 'new', - }, - alias2: [4], - alias3: 'new', - }, + aliases: ['productTemplate-1-update', 'productTemplate_1-update'], template: { template1: { name: 'template 1 - update', @@ -213,16 +202,7 @@ describe('UPDATE product template', () => { resJson.details.should.be.eql(body.param.details); resJson.disabled.should.be.eql(true); resJson.hidden.should.be.eql(true); - - resJson.aliases.should.be.eql({ - alias1: { - subAlias1A: 11, - subAlias1B: 2, - subAlias1C: 'new', - }, - alias2: [4], - alias3: 'new', - }); + resJson.aliases.should.be.eql(body.param.aliases); resJson.template.should.be.eql({ template1: { name: 'template 1 - update', diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js new file mode 100644 index 00000000..490bbba6 --- /dev/null +++ b/src/routes/projectMemberInvites/create.js @@ -0,0 +1,262 @@ + + +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, BUS_API_EVENT } 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(), + }, +}; + +/** + * Helper method to build promises for creating new invites in DB + * + * @param {Object} req express request object + * @param {Object} invite invite to process + * @param {Array} invites existent invites from DB + * @param {Object} data template for new invites to be put in DB + * + * @returns {Promise} list of promises + */ +const buildCreateInvitePromises = (req, invite, invites, data) => { + const invitePromises = []; + + if (invite.userIds) { + // remove invites for users that are invited already + _.remove(invite.userIds, u => _.some(invites, i => i.userId === u)); + invite.userIds.forEach((userId) => { + const dataNew = _.clone(data); + + dataNew.userId = userId; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + } + + if (invite.emails) { + // if for some emails there are already existent users, we will invite them by userId, + // to avoid sending them registration email + return util.lookupUserEmails(req, invite.emails) + .then((existentUsers) => { + // existent user we will invite by userId and email + const existentUsersWithNumberId = existentUsers.map((user) => { + const userWithNumberId = _.clone(user); + + userWithNumberId.id = parseInt(user.id, 10); + + return userWithNumberId; + }); + // non-existent users we will invite them by email only + const nonExistentUserEmails = invite.emails.filter(inviteEmail => + !_.find(existentUsers, { email: inviteEmail }), + ); + + // remove invites for users that are invited already + _.remove(existentUsersWithNumberId, user => _.some(invites, i => i.userId === user.id)); + existentUsersWithNumberId.forEach((user) => { + const dataNew = _.clone(data); + + dataNew.userId = user.id; + dataNew.email = user.email; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + + // remove invites for users that are invited already + _.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email)); + nonExistentUserEmails.forEach((email) => { + const dataNew = _.clone(data); + + dataNew.email = email; + + invitePromises.push(models.ProjectMemberInvite.create(dataNew)); + }); + + return Promise.resolve(invitePromises); + }).catch((error) => { + req.log.error(error); + return Promise.reject(invitePromises); + }); + } + + return Promise.resolve(invitePromises); +}; + +const sendInviteEmail = (req, projectId, invite) => { + const emailEventType = BUS_API_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED; + const promises = [ + models.Project.find({ + where: { id: projectId }, + raw: true, + }), + util.getMemberDetailsByUserIds([req.authUser.userId], req.log, req.id), + ]; + return Promise.all(promises).then((responses) => { + const project = responses[0]; + const initiator = responses[1] && responses[1].length ? responses[1][0] : { + userId: req.authUser.userId, + firstName: 'Connect', + lastName: 'User', + }; + createEvent(emailEventType, { + data: { + connectURL: config.get('connectUrl'), + accountsAppURL: config.get('accountsAppUrl'), + subject: config.get('inviteEmailSubject'), + projects: [{ + name: project.name, + projectId, + sections: [ + { + EMAIL_INVITES: true, + title: config.get('inviteEmailSectionTitle'), + projectName: project.name, + projectId, + initiator, + }, + ], + }], + }, + recipients: [invite.email], + version: 'v3', + from: { + name: config.get('EMAIL_INVITE_FROM_NAME'), + email: config.get('EMAIL_INVITE_FROM_EMAIL'), + }, + categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()], + }, req.log); + }).catch((error) => { + req.log.error(error); + }); +}; + +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, + }; + + return buildCreateInvitePromises(req, invite, invites, data) + .then((invitePromises) => { + if (invitePromises.length === 0) { + return []; + } + + 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 && !v.userId) { + sendInviteEmail(req, projectId, v); + } + }); + return values; + }); // models.sequelize.Promise.all + }); // buildCreateInvitePromises + }); // models.ProjectMemberInvite.getPendingInvitesForProject + }) + .then(values => res.status(201).json(util.wrapResponse(req.id, values, null, 201))) + .catch(err => next(err)); + }, +]; diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js new file mode 100644 index 00000000..e648a06f --- /dev/null +++ b/src/routes/projectMemberInvites/create.spec.js @@ -0,0 +1,637 @@ +/* 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(); + // restoring the stubs in beforeEach instead of afterEach because these methods are already stubbed + server.services.pubsub.init.restore(); + server.services.pubsub.publish.restore(); + sinon.stub(server.services.pubsub, 'init', () => {}); + sinon.stub(server.services.pubsub, 'publish', () => {}); + // by default mock lookupUserEmails return nothing so all the cases are not broken + sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([])); + sandbox.stub(util, 'getMemberDetailsByUserIds', () => Promise.resolve([{ + userId: 40051333, + firstName: 'Admin', + lastName: 'User', + }])); + }); + afterEach(() => { + sandbox.restore(); + }); + + 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 userId invite as customer for existent user when invite by email', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + util.lookupUserEmails.restore(); + sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([{ + id: '12345', + email: 'hello@world.com', + }])); + request(server) + .post(`/v4/projects/${project2.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['hello@world.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content[0]; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.projectId.should.equal(project2.id); + resJson.userId.should.equal(12345); + 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 201 and empty response when trying add already invited member', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.COPILOT, + }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userIds: [40051335], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.length.should.equal(0); + server.services.pubsub.publish.neverCalledWith('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/projectUpgrade/create.spec.js b/src/routes/projectUpgrade/create.spec.js index 41ee7e0b..9ab136e0 100644 --- a/src/routes/projectUpgrade/create.spec.js +++ b/src/routes/projectUpgrade/create.spec.js @@ -95,6 +95,7 @@ describe('Project upgrade', () => { name: 'name 1', productKey: 'a product key', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 5810401e..e8807e38 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -38,6 +38,7 @@ describe('Project create', () => { name: 'template 1', productKey: 'productKey-1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon2.ico', brief: 'brief 1', details: 'details 1', @@ -51,6 +52,7 @@ describe('Project create', () => { name: 'template 2', productKey: 'productKey-2', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon2.ico', brief: 'brief 2', details: 'details 2', @@ -64,6 +66,7 @@ describe('Project create', () => { name: 'template 3', productKey: 'productKey-3', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon3.ico', brief: 'brief 3', details: 'details 3', diff --git a/src/routes/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/routes/timelines/create.spec.js b/src/routes/timelines/create.spec.js index 41590c37..20c88ed8 100644 --- a/src/routes/timelines/create.spec.js +++ b/src/routes/timelines/create.spec.js @@ -45,6 +45,7 @@ const productTemplates = [ name: 'name 1', productKey: 'productKey 1', category: 'generic', + subCategory: 'generic', icon: 'http://example.com/icon1.ico', brief: 'brief 1', details: 'details 1', diff --git a/src/services/busApi.js b/src/services/busApi.js index ddc58656..0197523c 100644 --- a/src/services/busApi.js +++ b/src/services/busApi.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import config from 'config'; const Promise = require('bluebird'); @@ -54,8 +55,8 @@ function createEvent(topic, payload, logger) { payload, }).then((resp) => { logger.debug('Sent event to bus-api'); - logger.debug(`Sent event to bus-api [data]: ${resp.data}`); - logger.debug(`Sent event to bus-api [status]: ${resp.status}`); + logger.debug(`Sent event to bus-api [data]: ${_.get(resp, 'data')}`); + logger.debug(`Sent event to bus-api [status]: ${_.get(resp, 'status')}`); }).catch((error) => { logger.debug('Error sending event to bus-api'); if (error.response) { diff --git a/src/tests/seed.js b/src/tests/seed.js index 5c4f553d..8790ad96 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -326,6 +326,7 @@ models.sequelize.sync({ force: true }) name: 'name 1', productKey: 'productKey 1', category: 'category', + subCategory: 'category', icon: 'http://example.com/icon1.ico', question: 'question 1', info: 'info 1', diff --git a/src/util.js b/src/util.js index 542c2ee1..ea20e3be 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,84 @@ _.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); + }); + }), + + /** + * Lookup user handles from emails + * @param {Object} req request + * @param {Array} userEmails user emails + * @param {Boolean} isPattern flag to indicate that pattern matching is required or not + * @return {Promise} promise + */ + lookupUserEmails: (req, userEmails, isPattern = false) => { + req.log.debug(`identityServiceEndpoint: ${config.get('identityServiceEndpoint')}`); + let filter = _.map(userEmails, i => `email=${i}`).join(' OR '); + if (isPattern) { + filter += '&like=true'; + } + req.log.trace('filter for users api call', filter); + return util.getSystemUserToken(req.log) + .then((token) => { + req.log.debug(`Bearer ${token}`); + const httpClient = util.getHttpClient({ id: req.id, log: req.log }); + return httpClient.get(`${config.get('identityServiceEndpoint')}users`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + params: { + fields: 'handle,id,email', + filter, + }, + // set longer timeout as default 3000 could be not enough for identity service response + timeout: 5000, + }) + .then((response) => { + const data = _.get(response, 'data.result.content', null); + if (!data) { throw new Error('Response does not have result.content'); } + req.log.debug('UserHandle response', data); + return data; + }); + }); + }, }); 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" +