diff --git a/.circleci/config.yml b/.circleci/config.yml index 101a1bc599..7985d4ba23 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -340,7 +340,7 @@ workflows: filters: branches: only: - - develop + - recruit-proxy-api-aggapi # This is alternate dev env for parallel testing - "build-test": context : org-global @@ -368,7 +368,7 @@ workflows: filters: &filters-staging branches: only: - - develop + - recruit-proxy-api-aggapi # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration diff --git a/Dockerfile b/Dockerfile index 902b754e09..72ffa4d939 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,7 @@ ARG TC_M2M_GRANT_TYPE ARG TC_M2M_AUTH0_PROXY_SERVER_URL ARG TC_M2M_AUTH0_URL ARG AUTH_SECRET +ARG VALID_ISSUERS ARG COMMUNITY_APP_URL ARG GSHEETS_API_KEY @@ -124,6 +125,7 @@ ENV TC_M2M_GRANT_TYPE=$TC_M2M_GRANT_TYPE ENV TC_M2M_AUTH0_PROXY_SERVER_URL=$TC_M2M_AUTH0_PROXY_SERVER_URL ENV TC_M2M_AUTH0_URL=$TC_M2M_AUTH0_URL ENV AUTH_SECRET=$AUTH_SECRET +ENV VALID_ISSUERS=$VALID_ISSUERS ENV CONTENTFUL_MANAGEMENT_TOKEN=$CONTENTFUL_MANAGEMENT_TOKEN ENV CONTENTFUL_EDU_SPACE_ID=$CONTENTFUL_EDU_SPACE_ID diff --git a/build.sh b/build.sh index 725701d465..23730d8013 100755 --- a/build.sh +++ b/build.sh @@ -51,7 +51,8 @@ docker build -t $TAG \ --build-arg GROWSURF_CAMPAIGN_ID=$GROWSURF_CAMPAIGN_ID \ --build-arg GSHEETS_API_KEY=$GSHEETS_API_KEY \ --build-arg OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY \ - --build-arg COMMUNITY_APP_URL=$COMMUNITY_APP_URL . + --build-arg COMMUNITY_APP_URL=$COMMUNITY_APP_URL \ + --build-arg VALID_ISSUERS=$VALID_ISSUERS . # Copies "node_modules" from the created image, if necessary for caching. docker create --name app $TAG diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 75e275d959..75169457dd 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -83,6 +83,7 @@ module.exports = { JWT_AUTH: { SECRET: 'AUTH_SECRET', + AUTH_SECRET: 'AUTH_SECRET', VALID_ISSUERS: 'VALID_ISSUERS', }, diff --git a/config/default.js b/config/default.js index e2946ddf77..bc183806e0 100644 --- a/config/default.js +++ b/config/default.js @@ -429,5 +429,5 @@ module.exports = { OPTIMIZELY: { SDK_KEY: '7V4CJhurXT3Y3bnzv1hv1', }, - PLATFORM_SITE_URL: 'https://platform.topcoder.com', + PLATFORM_SITE_URL: 'https://platform.topcoder-dev.com', }; diff --git a/config/production.js b/config/production.js index f83da0fbc1..002b8340d8 100644 --- a/config/production.js +++ b/config/production.js @@ -214,4 +214,5 @@ module.exports = { TC_EDU_SEARCH_PATH: '/search', TC_EDU_SEARCH_BAR_MAX_RESULTS_EACH_GROUP: 3, ENABLE_RECOMMENDER: true, + PLATFORM_SITE_URL: 'https://platform.topcoder.com', }; diff --git a/docs/postman/Community APP.postman_collection.json b/docs/postman/Community APP.postman_collection.json new file mode 100644 index 0000000000..d0b3586dcb --- /dev/null +++ b/docs/postman/Community APP.postman_collection.json @@ -0,0 +1,549 @@ +{ + "info": { + "_postman_id": "95d8a75d-2520-494b-becd-f9f2e8537707", + "name": "Community APP", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ + { + "name": "Recruit Api", + "item": [ + { + "name": "Profile", + "item": [ + { + "name": "get profile successfully", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "get profile with invalid token 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_1}}", + "type": "text" + } + ], + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "get profile with invalid token 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_2}}", + "type": "text" + } + ], + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "get profile with invalid token 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_3}}", + "type": "text" + } + ], + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "get profile with invalid token 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_4}}", + "type": "text" + } + ], + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile successfully", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": [] + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "false", + "type": "text" + }, + { + "key": "city", + "value": "ABC", + "type": "text" + }, + { + "key": "countryName", + "value": "India", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with invalid token 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_1}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with invalid token 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_2}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with invalid token 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_3}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with invalid token 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{invalid_token_4}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with invalid field", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"availability\\\" must be a boolean\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + }, + { + "key": "availability", + "value": "asdd", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with missing field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"phone\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "availability", + "value": "asdd", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + }, + { + "name": "update profile with missing field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"availability\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./docs/postman/resume.txt" + }, + { + "key": "phone", + "value": "+1123226666", + "type": "text" + } + ] + }, + "url": "{{URL}}/recruit/profile" + }, + "response": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/postman/Community APP.postman_environment.json b/docs/postman/Community APP.postman_environment.json new file mode 100644 index 0000000000..5e5fc77710 --- /dev/null +++ b/docs/postman/Community APP.postman_environment.json @@ -0,0 +1,39 @@ +{ + "id": "c92b4b6b-ed64-4879-91de-e293128256b0", + "name": "Community APP", + "values": [ + { + "key": "URL", + "value": "localhost:3000/api", + "enabled": true + }, + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik5VSkZORGd4UlRVME5EWTBOVVkzTlRkR05qTXlRamxETmpOQk5UYzVRVUV3UlRFeU56TTJRUSJ9.eyJodHRwczovL3RvcGNvZGVyLWRldi5jb20vcm9sZXMiOlsiVG9wY29kZXIgVXNlciJdLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdXNlcklkIjoiODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vaGFuZGxlIjoiaXNiaWxpciIsImh0dHBzOi8vdG9wY29kZXItZGV2LmNvbS91c2VyX2lkIjoiYXV0aDB8ODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdGNzc28iOiI4ODc3NDYzNHw0ZDczMWRjODNiZTk5OThkOWE1MmUyOTA2OThmNGIwNGZiMmVjNjE1OTliODIxZmYxNjRjYWEzYzhhNmU3IiwiaHR0cHM6Ly90b3Bjb2Rlci1kZXYuY29tL2FjdGl2ZSI6dHJ1ZSwibmlja25hbWUiOiJpc2JpbGlyIiwibmFtZSI6ImVtcmUuaXNiaWxpckBnbWFpbC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvODE3NjNjMzE0ZGU0Y2ZiNGUxNDRhYzU3M2U1NmMxZjY_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZlbS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wNi0xOVQxNzowNjoyMi4yMzVaIiwiZW1haWwiOiJlbXJlLmlzYmlsaXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vYXV0aC50b3Bjb2Rlci1kZXYuY29tLyIsInN1YiI6ImF1dGgwfDg4Nzc0NjM0IiwiYXVkIjoiQlhXWFVXbmlsVlVQZE4wMXQyU2UyOVR3MlpZTkdadkgiLCJpYXQiOjE2MjQxMjUzMzYsImV4cCI6MTYyNDEyODMzNiwibm9uY2UiOiJjVE5DTFVwRlpXMVJhMm94WkhSdE5WaGpVbk5IVkZGVFdWTjRiRzFCYVVaQ1dHZGpSVWhZVTB0RVZnPT0ifQ.SfKTYecx7A2qbqM3v8lJaC1GSdctNhkTx6laAhe2BJYVX5LdVL82ZM3_9yWqqZDPWHS9ku408Jt1DUMo5vRev34Av0iwJpYIT_4BbmMLeWQGQ3aRB78zp_emkZ5TE1vOnfSJvfsPoQoiLIwohyOUaDzz9MGdjrgYuSEAA1RGdjhVcamFlNNbR-lNMffnWYs0oWi89N5kgvv37JZjpDPAvQbotNT80Vs0JrLr1p71JLFcLFTQJ-bthFmt0922dj47-1MSjUQgOzFMN4JNjZKGtExyLGR7s_ipCQxg9lxHjMD-JNwg0liI20YGfTXZv9QQQ_K2jPkXiatHBQq_v2_Qjg", + "enabled": true + }, + { + "key": "invalid_token_1", + "value": "eyJ", + "enabled": true + }, + { + "key": "invalid_token_2", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU", + "enabled": true + }, + { + "key": "invalid_token_3", + "value": "", + "enabled": true + }, + { + "key": "invalid_token_4", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2021-06-19T18:52:31.778Z", + "_postman_exported_using": "Postman/8.6.2" +} \ No newline at end of file diff --git a/docs/postman/resume.txt b/docs/postman/resume.txt new file mode 100644 index 0000000000..a0d9880d43 --- /dev/null +++ b/docs/postman/resume.txt @@ -0,0 +1 @@ +My Resume \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000000..abd8729743 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,189 @@ +openapi: 3.0.0 +info: + title: Community APP + description: Community APP + version: 1.0.0 +servers: + - url: http://local.topcoder-dev.com:3000 +tags: + - name: Profile +paths: + /api/recruit/profile: + get: + tags: + - Profile + description: | + Get Profile of current user + + **Authorization** All topcoder members are allowed. + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Profile" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/AuthError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/AuthError" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Profile" + "500": + description: Internal Server Error + content: + text/plain:: + schema: + type: string + post: + tags: + - Profile + description: | + Update Profile details of current user + **Authorization** All topcoder members are allowed. + security: + - bearerAuth: [] + requestBody: + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/ProfileUpdate" + responses: + "204": + description: OK + "400": + description: Bad request + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/JoiError" + - $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/AuthError" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/AuthError" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + text/plain:: + schema: + type: string +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + Profile: + required: + - availability + properties: + phone: + type: string + description: "The phone number of the user" + example: "+1123226666" + resume: + type: string + description: "The resume of the user" + availability: + type: boolean + description: "The availability of the user" + default: true + example: true + hasProfile: + type: boolean + description: "Whether has profile for the user" + ProfileUpdate: + required: + - phone + - availability + - city + - countryName + properties: + phone: + type: string + description: "The phone number of the user" + example: "(123) 456-7890" + city: + type: string + description: "The member's city" + countryName: + type: string + description: "The member's country" + resume: + type: string + format: binary + description: "The resume file of the user" + availability: + type: boolean + description: "The availability of the user" + example: true + Error: + properties: + error: + type: boolean + example: true + status: + type: integer + example: 404 + url: + type: string + format: uri + errObj: + type: object + JoiError: + required: + - message + properties: + message: + type: string + AuthError: + properties: + version: + type: string + result: + type: object + properties: + success: + type: boolean + example: false + status: + type: integer + example: 403 + content: + type: object + properties: + message: + type: string diff --git a/package-lock.json b/package-lock.json index d0f67f50af..d69bbb5673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -729,8 +729,8 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "buffer-from": "1.1.1", + "source-map": "0.6.1" } } } @@ -1333,6 +1333,144 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@optimizely/js-sdk-datafile-manager": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.8.1.tgz", + "integrity": "sha512-zMfyXQUqJlPoFGTNvreGSneGRnr5hn4jp03ofipIpA/RONNsf7DEi/H/uC4pAZxlYm1r5eHZRwKU6gwZTB31LQ==", + "requires": { + "@optimizely/js-sdk-logging": "0.1.0", + "@optimizely/js-sdk-utils": "0.4.0", + "decompress-response": "4.2.1" + }, + "dependencies": { + "@optimizely/js-sdk-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz", + "integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==", + "requires": { + "uuid": "3.4.0" + } + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "2.1.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + } + } + }, + "@optimizely/js-sdk-event-processor": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-event-processor/-/js-sdk-event-processor-0.8.2.tgz", + "integrity": "sha512-5sVcQFqgKF0R+vJbBXy6ykKTlEfll0Ti0xGeKU3TLILRNvPDxTpVAlyrLfBC/yfF/hopjRPusGp3z9lZnVej0w==", + "requires": { + "@optimizely/js-sdk-logging": "0.1.0", + "@optimizely/js-sdk-utils": "0.4.0" + }, + "dependencies": { + "@optimizely/js-sdk-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz", + "integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==", + "requires": { + "uuid": "3.4.0" + } + } + } + }, + "@optimizely/js-sdk-logging": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.1.0.tgz", + "integrity": "sha512-Bs2zHvsdNIk2QSg05P6mKIlROHoBIRNStbrVwlePm603CucojKRPlFJG4rt7sFZQOo8xS8I7z1BmE4QI3/ZE9A==", + "requires": { + "@optimizely/js-sdk-utils": "0.1.0" + } + }, + "@optimizely/js-sdk-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.1.0.tgz", + "integrity": "sha512-p7499GgVaX94YmkrwOiEtLgxgjXTPbUQsvETaAil5J7zg1TOA4Wl8ClalLSvCh+AKWkxGdkL4/uM/zfbxPSNNw==", + "requires": { + "uuid": "3.4.0" + } + }, + "@optimizely/optimizely-sdk": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@optimizely/optimizely-sdk/-/optimizely-sdk-4.6.0.tgz", + "integrity": "sha512-xhTWe3Jg4YFCy8VG0c0depur0wumhgD3AnlSnHrTnaDNjuCvmBVrYV3cV3RMt8OJoZZxwQvJ2SJKjj73LMplBQ==", + "requires": { + "@optimizely/js-sdk-datafile-manager": "0.8.1", + "@optimizely/js-sdk-event-processor": "0.8.2", + "@optimizely/js-sdk-logging": "0.1.0", + "@optimizely/js-sdk-utils": "0.4.0", + "json-schema": "0.2.3", + "murmurhash": "0.0.2" + }, + "dependencies": { + "@optimizely/js-sdk-utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.4.0.tgz", + "integrity": "sha512-QG2oytnITW+VKTJK+l0RxjaS5VrA6W+AZMzpeg4LCB4Rn4BEKtF+EcW/5S1fBDLAviGq/0TLpkjM3DlFkJ9/Gw==", + "requires": { + "uuid": "3.4.0" + } + } + } + }, + "@optimizely/react-sdk": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@optimizely/react-sdk/-/react-sdk-2.6.0.tgz", + "integrity": "sha512-+f7RCbZ0VmtvbAAA4APq5fglj9gSiP+ibaAEKsBa5rTaWaN5I/SH2+4ywpfjpRNvYXobjAN7YxEXSvXPPTGeig==", + "requires": { + "@optimizely/js-sdk-logging": "0.1.0", + "@optimizely/optimizely-sdk": "4.6.0", + "hoist-non-react-statics": "3.3.2", + "prop-types": "15.7.2", + "utility-types": "3.10.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "16.13.1" + } + } + } + }, + "@sideway/address": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.2.tgz", + "integrity": "sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==", + "requires": { + "@hapi/hoek": "9.2.0" + }, + "dependencies": { + "@hapi/hoek": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", + "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==" + } + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@tanem/svg-injector": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-1.2.1.tgz", @@ -13163,19 +13301,29 @@ } }, "joi": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", - "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz", + "integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==", "requires": { - "hoek": "5.0.4", - "isemail": "3.2.0", - "topo": "3.0.3" + "@hapi/hoek": "9.2.0", + "@hapi/topo": "5.0.0", + "@sideway/address": "4.1.2", + "@sideway/formula": "3.0.0", + "@sideway/pinpoint": "2.0.0" }, "dependencies": { - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" + "@hapi/hoek": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", + "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "9.2.0" + } } } }, @@ -14256,8 +14404,8 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", "requires": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" + "pseudomap": "1.0.2", + "yallist": "2.1.2" } } } @@ -14965,6 +15113,11 @@ "xtend": "4.0.2" } }, + "murmurhash": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz", + "integrity": "sha1-bwe9ihEF5wnCb8iUIMtZMMJFhf4=" + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -15056,7 +15209,7 @@ "dev": true }, "navigation-component": { - "version": "github:topcoder-platform/navigation-component#ad2d6101c4fe10cc98b6ac1389c28b8b80941de7", + "version": "github:topcoder-platform/navigation-component#a8855ad57a4e6dcb569a244bc5ad4aff083c7a3c", "requires": { "classnames": "2.2.6", "lodash": "4.17.15", @@ -15067,9 +15220,9 @@ }, "dependencies": { "config": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.3.tgz", - "integrity": "sha512-T3RmZQEAji5KYqUQpziWtyGJFli6Khz7h0rpxDwYNjSkr5ynyTWwO7WpfjHzTXclNCDfSWQRcwMb+NwxJesCKw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.6.tgz", + "integrity": "sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg==", "requires": { "json5": "2.2.0" } @@ -15104,12 +15257,11 @@ } }, "redux": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", - "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz", + "integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==", "requires": { - "loose-envify": "1.4.0", - "symbol-observable": "1.2.0" + "@babel/runtime": "7.9.6" } }, "serialize-javascript": { @@ -15128,7 +15280,7 @@ "command-line-args": "5.1.1", "command-line-usage": "5.0.5", "compression": "1.7.4", - "config": "3.3.3", + "config": "3.3.6", "cookie-parser": "1.4.5", "cross-env": "5.2.1", "express": "4.17.1", @@ -15145,7 +15297,7 @@ "react-helmet": "5.2.1", "react-redux": "6.0.1", "react-router-dom": "4.3.1", - "redux": "4.0.5", + "redux": "4.1.0", "redux-actions": "2.6.5", "redux-devtools": "3.5.0", "redux-devtools-dock-monitor": "1.1.3", @@ -23614,13 +23766,13 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "topcoder-react-lib": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.1.6.tgz", - "integrity": "sha512-i9Btnbd9k6dq569avQA4vz0fIxz/nIUxJ9hMldN+n71JY3Qv9rNL6nVCk3ou/j7mMLwTY3zNajJsUAogjD4uIA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.1.8.tgz", + "integrity": "sha512-+Op873W6dVze1IpgevXgVN/ikw0IQm00D5GJTI19cD8drx8dZgMgZLk/LP6ydpcL2OQVxCDEYiwOf+VdIigfrg==", "requires": { "@topcoder-platform/tc-auth-lib": "git+https://github.com/topcoder-platform/tc-auth-lib.git#68fdc22464810c51b703a33e529cdbd6d09437de", "auth0-js": "6.8.4", - "config": "3.3.3", + "config": "3.3.6", "isomorphic-fetch": "2.2.1", "le_node": "1.8.0", "lodash": "4.17.15", @@ -23641,13 +23793,13 @@ "@topcoder-platform/tc-auth-lib": { "version": "git+https://github.com/topcoder-platform/tc-auth-lib.git#68fdc22464810c51b703a33e529cdbd6d09437de", "requires": { - "lodash": "4.17.20" + "lodash": "4.17.21" }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -23656,19 +23808,18 @@ "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-6.8.4.tgz", "integrity": "sha1-Qw3Uystk2NFdabHmIRhPmipkCmE=", "requires": { - "Base64": "~0.1.3", + "Base64": "0.1.4", "json-fallback": "0.0.1", - "jsonp": "~0.0.4", - "qs": "git+https://github.com/jfromaniello/node-querystring.git#fix_ie7_bug_with_arrays", - "reqwest": "^1.1.4", - "trim": "~0.0.1", - "winchan": "^0.1.1", - "xtend": "~2.1.1" + "jsonp": "0.0.4", + "qs": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8", + "reqwest": "1.1.6", + "trim": "0.0.1", + "winchan": "0.1.4", + "xtend": "2.1.2" }, "dependencies": { "qs": { - "version": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8", - "from": "git+https://github.com/jfromaniello/node-querystring.git#fix_ie7_bug_with_arrays" + "version": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8" } } }, @@ -23681,9 +23832,9 @@ } }, "config": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.3.tgz", - "integrity": "sha512-T3RmZQEAji5KYqUQpziWtyGJFli6Khz7h0rpxDwYNjSkr5ynyTWwO7WpfjHzTXclNCDfSWQRcwMb+NwxJesCKw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.6.tgz", + "integrity": "sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg==", "requires": { "json5": "2.2.0" } @@ -23701,8 +23852,8 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", "requires": { - "debug": "^2.2.0", - "stream-consume": "^0.1.0" + "debug": "2.6.9", + "stream-consume": "0.1.1" }, "dependencies": { "debug": { @@ -23725,7 +23876,20 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "requires": { - "react-is": "^16.7.0" + "react-is": "16.13.1" + } + }, + "idtoken-verifier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.2.0.tgz", + "integrity": "sha512-D+KZkWx+4fDsLXrWIfwWOUAdEQczkqzs0REN0qpJ+9axG4kOeFFXXAFEmSfDErGh8dvM4vY8dQRROw9g8ZnNbw==", + "requires": { + "base64-js": "1.3.1", + "crypto-js": "3.3.0", + "es6-promise": "4.2.8", + "jsbn": "1.1.0", + "unfetch": "4.2.0", + "url-join": "4.0.1" } }, "json5": { @@ -23745,9 +23909,9 @@ } }, "mime": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", - "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" }, "ms": { "version": "2.1.2", @@ -23764,12 +23928,12 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz", "integrity": "sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==", "requires": { - "@babel/runtime": "^7.3.1", - "hoist-non-react-statics": "^3.3.0", - "invariant": "^2.2.4", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^16.8.2" + "@babel/runtime": "7.9.6", + "hoist-non-react-statics": "3.3.2", + "invariant": "2.2.4", + "loose-envify": "1.4.0", + "prop-types": "15.7.2", + "react-is": "16.13.1" } }, "readable-stream": { @@ -23783,9 +23947,9 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "requires": { "lru-cache": "6.0.0" } @@ -23800,23 +23964,23 @@ "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.2", - "debug": "^4.1.1", - "fast-safe-stringify": "^2.0.7", - "form-data": "^3.0.0", - "formidable": "^1.2.2", - "methods": "^1.1.2", - "mime": "^2.4.6", - "qs": "^6.9.4", - "readable-stream": "^3.6.0", - "semver": "^7.3.2" + "component-emitter": "1.3.0", + "cookiejar": "2.1.2", + "debug": "4.3.1", + "fast-safe-stringify": "2.0.7", + "form-data": "3.0.0", + "formidable": "1.2.2", + "methods": "1.1.2", + "mime": "2.5.2", + "qs": "6.9.4", + "readable-stream": "3.6.0", + "semver": "7.3.5" } }, "tc-core-library-js": { "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", "requires": { - "auth0-js": "9.14.3", + "auth0-js": "9.16.2", "axios": "0.12.0", "bunyan": "1.8.15", "jsonwebtoken": "8.5.1", @@ -23828,17 +23992,17 @@ }, "dependencies": { "auth0-js": { - "version": "9.14.3", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.14.3.tgz", - "integrity": "sha512-UO/fGv9641PUpYjz2nkPaUHzzrhNaJKupJOqt8blj1pD6wBgpZtxUSXBox6Y8md3eTBzpxeWxV+6RKzzERvr1g==", + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.16.2.tgz", + "integrity": "sha512-cF1nRjmMDezmhJ+ZwwYp23F0gPqU0zNmF/VvTpcwvCrEMl9lAvkCd4iburN1I7G8SYaaIYEfcGedCphpDZw6OQ==", "requires": { - "base64-js": "^1.3.0", - "idtoken-verifier": "^2.0.3", - "js-cookie": "^2.2.0", - "qs": "^6.7.0", - "superagent": "^5.3.1", - "url-join": "^4.0.1", - "winchan": "^0.2.2" + "base64-js": "1.3.1", + "idtoken-verifier": "2.2.0", + "js-cookie": "2.2.1", + "qs": "6.9.4", + "superagent": "5.3.1", + "url-join": "4.0.1", + "winchan": "0.2.2" } }, "winchan": { @@ -23853,38 +24017,38 @@ "resolved": "https://registry.npmjs.org/topcoder-react-utils/-/topcoder-react-utils-0.7.5.tgz", "integrity": "sha512-/jolO/UUCC/FL/MniBMFi9d7Wc1KbzwvgT5STGs4T+7u7R26bQugGPpGVISEPuglsmW0Xybh6iRi+pT/muOkbg==", "requires": { - "babel-runtime": "^6.26.0", - "body-parser": "^1.18.3", - "command-line-args": "^5.0.2", - "command-line-usage": "^5.0.5", - "compression": "^1.7.2", - "config": "^1.30.0", - "cookie-parser": "^1.4.3", - "express": "^4.16.3", - "helmet": "^3.12.1", - "lodash": "^4.17.10", - "moment": "^2.22.2", - "morgan": "^1.9.0", - "node-forge": "^0.7.5", - "prop-types": "^15.6.2", - "raf": "^3.4.0", - "react": "^16.4.1", - "react-css-super-themr": "^2.2.0", - "react-dom": "^16.4.1", - "react-helmet": "^5.2.0", - "react-redux": "^5.0.7", - "react-router-dom": "^4.3.1", - "redux": "^3.7.2", - "redux-actions": "^2.4.0", - "redux-devtools": "^3.4.1", - "redux-devtools-dock-monitor": "^1.1.3", - "redux-devtools-log-monitor": "^1.4.0", - "redux-promise": "^0.6.0", - "request-ip": "^2.0.2", - "serialize-javascript": "^1.5.0", - "serve-favicon": "^2.5.0", - "shortid": "^2.2.8", - "url-parse": "^1.4.1" + "babel-runtime": "6.26.0", + "body-parser": "1.19.0", + "command-line-args": "5.1.1", + "command-line-usage": "5.0.5", + "compression": "1.7.4", + "config": "1.31.0", + "cookie-parser": "1.4.5", + "express": "4.17.1", + "helmet": "3.22.0", + "lodash": "4.17.15", + "moment": "2.25.3", + "morgan": "1.10.0", + "node-forge": "0.7.6", + "prop-types": "15.7.2", + "raf": "3.4.1", + "react": "16.13.1", + "react-css-super-themr": "2.3.0", + "react-dom": "16.13.1", + "react-helmet": "5.2.1", + "react-redux": "5.1.2", + "react-router-dom": "4.3.1", + "redux": "3.7.2", + "redux-actions": "2.6.5", + "redux-devtools": "3.5.0", + "redux-devtools-dock-monitor": "1.1.3", + "redux-devtools-log-monitor": "1.4.0", + "redux-promise": "0.6.0", + "request-ip": "2.1.3", + "serialize-javascript": "1.9.1", + "serve-favicon": "2.5.0", + "shortid": "2.2.15", + "url-parse": "1.4.7" }, "dependencies": { "config": { @@ -23892,7 +24056,7 @@ "resolved": "https://registry.npmjs.org/config/-/config-1.31.0.tgz", "integrity": "sha512-Ep/l9Rd1J9IPueztJfpbOqVzuKHQh4ZODMNt9xqTYdBBNRXbV4oTu34kCkkfdRVcDq0ohtpaeXGgb+c0LQxFRA==", "requires": { - "json5": "^1.0.1" + "json5": "1.0.1" } }, "json5": { @@ -23900,7 +24064,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { - "minimist": "^1.2.0" + "minimist": "1.2.5" } }, "react-redux": { @@ -23908,13 +24072,13 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", "integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==", "requires": { - "@babel/runtime": "^7.1.2", - "hoist-non-react-statics": "^3.3.0", - "invariant": "^2.2.4", - "loose-envify": "^1.1.0", - "prop-types": "^15.6.1", - "react-is": "^16.6.0", - "react-lifecycles-compat": "^3.0.0" + "@babel/runtime": "7.9.6", + "hoist-non-react-statics": "3.3.2", + "invariant": "2.2.4", + "loose-envify": "1.4.0", + "prop-types": "15.7.2", + "react-is": "16.13.1", + "react-lifecycles-compat": "3.0.4" } } } @@ -23929,7 +24093,7 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", "requires": { - "object-keys": "~0.4.0" + "object-keys": "0.4.0" } }, "yallist": { @@ -24480,6 +24644,11 @@ "object.getownpropertydescriptors": "2.1.0" } }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -26205,6 +26374,25 @@ "hoek": "4.2.1", "joi": "13.7.0", "node-expat": "2.3.18" + }, + "dependencies": { + "joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "requires": { + "hoek": "5.0.4", + "isemail": "3.2.0", + "topo": "3.0.3" + }, + "dependencies": { + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" + } + } + } } }, "xmlbuilder": { diff --git a/package.json b/package.json index 8f0d6fee5a..7c188001fd 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "i18n-iso-countries": "^3.7.1", "immutable": "^3.8.2", "isomorphic-fetch": "^2.2.1", + "joi": "^17.4.0", "js-beautify": "^1.10.3", "le_node": "^1.7.0", "localStorage": "^1.0.4", diff --git a/src/server/routes/recruitCRM.js b/src/server/routes/recruitCRM.js index 34c1831539..5e9f70a658 100644 --- a/src/server/routes/recruitCRM.js +++ b/src/server/routes/recruitCRM.js @@ -3,8 +3,13 @@ */ import express from 'express'; +import { middleware } from 'tc-core-library-js'; +import config from 'config'; +import _ from 'lodash'; import RecruitCRMService from '../services/recruitCRM'; +const authenticator = middleware.jwtAuthenticator; +const authenticatorOptions = _.pick(config.SECRET.JWT_AUTH, ['AUTH_SECRET', 'VALID_ISSUERS']); const cors = require('cors'); const multer = require('multer'); @@ -27,5 +32,7 @@ routes.get('/jobs/search', (req, res, next) => new RecruitCRMService().getJobs(r routes.get('/jobs/:id', (req, res, next) => new RecruitCRMService().getJob(req, res, next)); routes.post('/jobs/:id/apply', upload.single('resume'), (req, res, next) => new RecruitCRMService().applyForJob(req, res, next)); routes.get('/candidates/search', (req, res, next) => new RecruitCRMService().searchCandidates(req, res, next)); - +// new router added +routes.get('/profile', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), (req, res, next) => new RecruitCRMService().getProfile(req, res, next)); +routes.post('/profile', (req, res, next) => authenticator(authenticatorOptions)(req, res, next), upload.single('resume'), (req, res, next) => new RecruitCRMService().updateProfile(req, res, next)); export default routes; diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index c352dd0dc5..8eb1b0fc51 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -6,6 +6,7 @@ import config from 'config'; import qs from 'qs'; import _ from 'lodash'; import { logger } from 'topcoder-react-lib'; +import Joi from 'joi'; import GrowsurfService from './growsurf'; import { sendEmailDirect } from './sendGrid'; @@ -62,6 +63,13 @@ function notifyKirilAndNick(error) { }); } +const updateProfileSchema = Joi.object().keys({ + phone: Joi.string().required(), + availability: Joi.boolean().required(), + city: Joi.string().required(), + countryName: Joi.string().required(), +}).required(); + /** * Auxiliary class that handles communication with recruitCRM */ @@ -449,4 +457,164 @@ export default class RecruitCRMService { return next(err); } } + + /** + * Get user profile endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async getProfile(req, res, next) { + try { + // get candidate by email + const candidate = await this.getCandidateByEmail(req.authUser.email); + // return error if getCandidateByEmail operation failed + if (candidate.error) { + const error = candidate; + logger.error(error); + const responseNoProfileMapping = { + hasProfile: false, + }; + return res.send(responseNoProfileMapping); + } + // apply desired response format + const responseMapping = { + hasProfile: true, + phone: candidate.contact_number, + resume: candidate.resume, + availability: _.isNil(candidate.available_from) ? true + : new Date(candidate.available_from) <= new Date(), + }; + return res.send(responseMapping); + } catch (err) { + return next(err); + } + } + + /** + * Update user profile endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async updateProfile(req, res, next) { + const { body, file } = req; + // validate provided data + const validationResult = updateProfileSchema.validate(body); + if (validationResult.error) { + return res.status(400).send({ message: validationResult.error.message }); + } + const fileData = new FormData(); + if (file) { + fileData.append('resume', file.buffer, file.originalname); + } + try { + // get candidate by email + const candidate = await this.getCandidateByEmail(req.authUser.email); + // return error if getCandidateByEmail operation failed + if (candidate.error) { + const error = candidate; + logger.error(error); + return res.status(error.status).send(error); + } + const candidateSlug = candidate.slug; + const form = { + city: body.city, + locality: body.countryName, + contact_number: body.phone, + available_from: body.availability === 'true' ? new Date().toISOString() : new Date('2100-01-01').toISOString(), + }; + // update candidate profile + const response = await fetch(`${this.private.baseUrl}/v1/candidates/${candidateSlug}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: this.private.authorization, + }, + body: JSON.stringify(form), + }); + if (response.status >= 300) { + const error = { + error: true, + status: response.status, + url: `${this.private.baseUrl}/v1/candidates${candidateSlug}`, + form, + errorObj: await response.json(), + }; + logger.error(error); + return res.status(error.status).send(error); + } + // Attach resume to candidate if uploaded + if (file) { + const formHeaders = fileData.getHeaders(); + const fileResponse = await fetch(`${this.private.baseUrl}/v1/candidates/${candidateSlug}`, { + method: 'POST', + headers: { + Authorization: this.private.authorization, + ...formHeaders, + }, + body: fileData, + }); + if (fileResponse.status >= 300) { + const error = { + error: true, + status: fileResponse.status, + url: `${this.private.baseUrl}/v1/candidates/${candidateSlug}`, + file, + formHeaders, + errorObj: await fileResponse.json(), + }; + logger.error(error); + return res.status(error.status).send(error); + } + } + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + + /** + * Get candidate by email + * @return {object} result of the search operation + * @param {string} email email address of the user. + */ + async getCandidateByEmail(email) { + const query = { + email, + }; + const url = `${this.private.baseUrl}/v1/candidates/search?${qs.stringify(query)}`; + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: this.private.authorization, + }, + }); + if (response.status === 429) { + await new Promise(resolve => setTimeout(resolve, 30000)); // wait 30sec + return this.getCandidateByEmail(email); + } + if (response.status >= 300) { + const error = { + error: true, + status: response.status, + url, + errObj: await response.json(), + }; + return error; + } + const data = await response.json(); + // return error object if candidate with provided email not found + if ((_.isArray(data) && data.length === 0) || data.data.length === 0) { + const error = { + error: true, + status: 404, + url, + errObj: { + message: `No candidate was found with email: ${email}`, + }, + }; + return error; + } + // return first candidate + return data.data[0]; + } } diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js index cdcb6d2260..2d6bf63d24 100644 --- a/src/shared/actions/recruitCRM.js +++ b/src/shared/actions/recruitCRM.js @@ -22,6 +22,18 @@ async function getJobsDone(query) { }; } +function getJobApplicationsInit() { + return {}; +} + +async function getJobApplicationsDone(tokenV3) { + const ss = new Service(); + const res = await ss.getJobApplications(tokenV3); + return { + data: res, + }; +} + /** * Job fetch init */ @@ -163,5 +175,7 @@ export default redux.createActions({ APPLY_FOR_JOB_DONE: applyForJobDone, SEARCH_CANDIDATES_INIT: searchCandidatesInit, SEARCH_CANDIDATES_DONE: searchCandidatesDone, + GET_JOB_APPLICATIONS_INIT: getJobApplicationsInit, + GET_JOB_APPLICATIONS_DONE: getJobApplicationsDone, }, }); diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index c7ef512a77..3c801ae2d2 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -108,7 +108,10 @@ export default function GigApply(props) { VIEW OTHER GIGS ) : ( - GO TO GIGS LIST + + GO TO GIGS LIST + CHECK GIG APPLICATION STATUS + ) } diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss index ec860228fc..abcea04f97 100644 --- a/src/shared/components/Gigs/GigApply/style.scss +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -313,6 +313,9 @@ margin-left: 20px; } } + .gig-list-btn { + margin-right: 12px; + } } /* stylelint-enable */ } diff --git a/src/shared/components/Gigs/GigHeader/index.jsx b/src/shared/components/Gigs/GigHeader/index.jsx new file mode 100644 index 0000000000..519415c88b --- /dev/null +++ b/src/shared/components/Gigs/GigHeader/index.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { config, Link } from 'topcoder-react-utils'; +import PT from 'prop-types'; +import BannerInfoIcon from 'assets/images/banner-info.svg'; +import './style.scss'; + +const GigHeader = ({ appNum }) => ( +
+
+
+ +
+ + You have {appNum} applied Gigs in the system + +
+ +
+ CHECK GIG APPLICATION STATUS +
+
+); + +GigHeader.defaultProps = { + appNum: 0, +}; + +GigHeader.propTypes = { + appNum: PT.number, +}; +export default GigHeader; diff --git a/src/shared/components/Gigs/GigHeader/style.scss b/src/shared/components/Gigs/GigHeader/style.scss new file mode 100644 index 0000000000..8c51a60079 --- /dev/null +++ b/src/shared/components/Gigs/GigHeader/style.scss @@ -0,0 +1,83 @@ +@import "~styles/mixins"; +@import "~components/GUIKit/Assets/Styles/default"; +@import "~components/Contentful/default"; + +.gig-header { + @include roboto-medium; + + @include xs-to-sm { + height: 81px; + line-height: 22px; + padding-left: 20px; + } + + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + margin-top: 15px; + height: 70px; + background: linear-gradient(90deg, #6f308b 0%, #1470ac 100%); + border-radius: 10px; + color: #fff; + font-size: 16px; + + .content { + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + color: #fff; + + .banner-info { + width: 24px; + height: 24px; + margin-left: 20px; + margin-right: 10px; + + @include xs-to-sm { + display: none; + } + } + + @include xs-to-sm { + width: 80%; + } + } + + .row-btn { + display: flex; + justify-content: flex-end; + flex: 1; + padding-right: 20px; + + @include xs-to-sm { + justify-content: flex-start; + } + + button.primary-white-md { + outline: none; + + @include primary-white; + @include md; + + &:hover { + @include primary-white; + } + } + } + + .banner-close { + width: 14px; + height: 14px; + margin-right: 30px; + + &:hover { + cursor: pointer; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + } +} diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index 337bef5414..14afad81dc 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -17,6 +17,7 @@ import IconBlackLocation from 'assets/images/icon-black-location.svg'; import { config, Link, isomorphy } from 'topcoder-react-utils'; import { getQuery, updateQuery } from 'utils/url'; import { withOptimizely } from '@optimizely/react-sdk'; +import GigHeader from 'components/Gigs/GigHeader'; import './jobLisingStyles.scss'; const cookies = require('browser-cookies'); @@ -55,6 +56,8 @@ class RecruitCRMJobsContainer extends React.Component { const { getJobs, jobs, + getJobApplications, + auth, } = this.props; const { state } = this; const q = getQuery(); @@ -73,6 +76,9 @@ class RecruitCRMJobsContainer extends React.Component { }; this.setState(stateUpdate); } + if (auth.tokenV3) { + getJobApplications(auth.tokenV3); + } } /** @@ -137,6 +143,8 @@ class RecruitCRMJobsContainer extends React.Component { loading, jobs, optimizely, + applications, + auth, } = this.props; const { term, @@ -249,6 +257,7 @@ class RecruitCRMJobsContainer extends React.Component { + {auth.tokenV3 && applications > 0 && }
{ jobsToDisplay.length @@ -288,6 +297,8 @@ class RecruitCRMJobsContainer extends React.Component { RecruitCRMJobsContainer.defaultProps = { jobs: [], loading: true, + applications: 0, + auth: {}, }; RecruitCRMJobsContainer.propTypes = { @@ -295,6 +306,9 @@ RecruitCRMJobsContainer.propTypes = { loading: PT.bool, jobs: PT.arrayOf(PT.shape), optimizely: PT.shape().isRequired, + getJobApplications: PT.func.isRequired, + applications: PT.number, + auth: PT.object, }; function mapStateToProps(state) { @@ -302,6 +316,10 @@ function mapStateToProps(state) { return { jobs: data ? data.jobs : [], loading: data ? data.loading : true, + applications: data.applications, + auth: { + ...state.auth, + }, }; } @@ -312,6 +330,10 @@ function mapDispatchToActions(dispatch) { dispatch(a.getJobsInit(ownProps)); dispatch(a.getJobsDone(ownProps)); }, + getJobApplications: (tokenV3) => { + dispatch(a.getJobApplicationsInit()); + dispatch(a.getJobApplicationsDone(tokenV3)); + }, }; } diff --git a/src/shared/reducers/recruitCRM.js b/src/shared/reducers/recruitCRM.js index 4a7bbfa629..9942cb2602 100644 --- a/src/shared/reducers/recruitCRM.js +++ b/src/shared/reducers/recruitCRM.js @@ -112,6 +112,20 @@ function onSearchCandidatesDone(state, { payload }) { return r; } +function onGetJobApplicationsInit(state) { + return { + ...state, + applications: 0, + }; +} + +function onGetJobApplicationsDone(state, { payload }) { + return { + ...state, + applications: payload.data, + }; +} + /** * Creates recruitCRM reducer with the specified initial state. * @param {Object} state Optional. If not given, the default one is @@ -128,6 +142,8 @@ function create(state = {}) { [actions.recruit.applyForJobDone]: onApplyForJobDone, [actions.recruit.searchCandidatesInit]: onSearchCandidatesInit, [actions.recruit.searchCandidatesDone]: onSearchCandidatesDone, + [actions.recruit.getJobApplicationsInit]: onGetJobApplicationsInit, + [actions.recruit.getJobApplicationsDone]: onGetJobApplicationsDone, }, state); } diff --git a/src/shared/services/recruitCRM.js b/src/shared/services/recruitCRM.js index c71bae877b..f8b60be687 100644 --- a/src/shared/services/recruitCRM.js +++ b/src/shared/services/recruitCRM.js @@ -1,5 +1,6 @@ import fetch from 'isomorphic-fetch'; import { logger } from 'topcoder-react-lib'; +import { config } from 'topcoder-react-utils'; import qs from 'qs'; import _ from 'lodash'; @@ -47,6 +48,29 @@ export default class Service { return res.json(); } + /** + * get member applications + * @param {*} tokenV3 + * @returns + */ + /* eslint-disable class-methods-use-this */ + async getJobApplications(tokenV3) { + const res = await fetch( + `${config.PLATFORM_SITE_URL}/earn-app/api/my-gigs/myJobApplications?page=1&perPage=1`, + { + method: 'GET', + headers: new Headers({ + Authorization: `Bearer ${tokenV3}`, + }), + }, + ); + if (!res.ok) { + const error = new Error('Failed to get job applications'); + logger.error(error, res); + } + return parseInt(res.headers.get('x-total'), 10) || 0; + } + /** * applyForJob for candidate * @param {string} id The job id to apply to