diff --git a/config/default.js b/config/default.js index 333d5f55..b2678ce5 100644 --- a/config/default.js +++ b/config/default.js @@ -17,6 +17,8 @@ module.exports = { TC_API: process.env.TC_API || 'https://api.topcoder-dev.com/v5', ORG_ID: process.env.ORG_ID || '36ed815b-3da1-49f1-a043-aaed0a4e81ad', + TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', + DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', DB_SCHEMA_NAME: process.env.DB_SCHEMA_NAME || 'bookings', PROJECT_API_URL: process.env.PROJECT_API_URL || 'https://api.topcoder-dev.com', diff --git a/package-lock.json b/package-lock.json index 6f940aa0..a95efc87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -908,6 +908,65 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "auth0-js": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.14.0.tgz", + "integrity": "sha512-40gIBUejmYAYse06ck6sxdNO0KU0pX+KDIQsWAkcyFtI0HU6dY5aeHxZfVYkYjtbArKr5s13LuZFdKrUiGyCqQ==", + "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" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "superagent": { + "version": "5.3.1", + "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" + }, + "dependencies": { + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } + } + } + } + }, "available-typed-arrays": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", @@ -1516,6 +1575,11 @@ } } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -1812,6 +1876,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -2793,6 +2862,26 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "idtoken-verifier": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.1.0.tgz", + "integrity": "sha512-X0423UM4Rc5bFb39Ai0YHr35rcexlu4oakKdYzSGZxtoPy84P86hhAbzlpgbgomcLOFRgzgKRvhY7YjO5g8OPA==", + "requires": { + "base64-js": "^1.3.0", + "crypto-js": "^3.2.1", + "es6-promise": "^4.2.8", + "jsbn": "^1.1.0", + "unfetch": "^4.1.0", + "url-join": "^4.0.1" + }, + "dependencies": { + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" + } + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -3355,6 +3444,11 @@ "@hapi/topo": "^5.0.0" } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3550,6 +3644,31 @@ "package-json": "^6.3.0" } }, + "le_node": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", + "integrity": "sha512-NXzjxBskZ4QawTNwlGdRG05jYU0LhV2nxxmP3x7sRMHyROV0jPdyyikO9at+uYrWX3VFt0Y/am11oKITedx0iw==", + "requires": { + "babel-runtime": "6.6.1", + "codependency": "0.1.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.11", + "reconnect-core": "1.3.0", + "semver": "5.1.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "semver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + } + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5653,6 +5772,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stream-consume": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", + "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==" + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -5817,17 +5941,37 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#081138e1f5eae76171abeff34b8f326b3fb2b504", - "from": "github:appirio-tech/tc-core-library-js#v2.6.5", + "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", + "from": "github:appirio-tech/tc-core-library-js#v2.6", "requires": { - "axios": "^0.19.0", + "auth0-js": "^9.4.2", + "axios": "^0.12.0", "bunyan": "^1.8.12", - "jsonwebtoken": "^8.5.1", - "jwks-rsa": "^1.6.0", - "lodash": "^4.17.15", + "jsonwebtoken": "^8.3.0", + "jwks-rsa": "^1.3.0", + "le_node": "^1.3.1", + "lodash": "^4.17.10", "millisecond": "^0.1.2", - "r7insight_node": "^1.8.4", "request": "^2.88.0" + }, + "dependencies": { + "axios": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", + "requires": { + "follow-redirects": "0.0.7" + } + }, + "follow-redirects": { + "version": "0.0.7", + "resolved": "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" + } + } } }, "term-size": { @@ -6008,6 +6152,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" }, + "unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, "uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", @@ -6125,6 +6274,11 @@ } } }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -6275,6 +6429,11 @@ "string-width": "^4.0.0" } }, + "winchan": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", + "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" + }, "winston": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", diff --git a/package.json b/package.json index bad6ecac..7eed4400 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "rewire": "^5.0.0", "sequelize": "^6.3.5", "superagent": "^6.1.0", - "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.5", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6", "util": "^0.12.3", "uuid": "^8.3.1", "winston": "^3.3.3" diff --git a/src/common/helper.js b/src/common/helper.js index 4df5766b..492c7ac1 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -23,6 +23,11 @@ const m2mAuth = require('tc-core-library-js').auth.m2m // const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_PROXY_SERVER_URL'])) const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL'])) +const topcoderM2M = m2mAuth({ + AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_FOR_BUS_API, + ..._.pick(config, ['AUTH0_URL', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL']) +}) + let busApiClient /** @@ -202,6 +207,14 @@ const getM2Mtoken = async () => { return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } +/* + * Function to get M2M token to access topcoder resources(e.g. /v3/users) + * @returns {Promise} + */ +const getTopcoderM2MToken = async () => { + return await topcoderM2M.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) +} + /** * Function to encode query string * @param {Object} queryObj the query object @@ -307,6 +320,27 @@ async function getProjects (token) { }) } +/** + * Get topcoder user by id from /v3/users. + * + * @param {String} userId the legacy user id + * @returns {Object} the user + */ +async function getTopcoderUserById (userId) { + const token = await getTopcoderM2MToken() + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ filter: `id=${userId}` }) + .set('Authorization', `Bearer ${token}`) + .set('Accept', 'application/json') + localLogger.debug({ context: 'getTopcoderUserById', message: `response body: ${JSON.stringify(res.body)}` }) + const user = _.get(res.body, 'result.content[0]') + if (!user) { + throw new errors.NotFoundError(`userId: ${userId} "user" not found from ${config.TOPCODER_USERS_API}`) + } + return user +} + /** * Function to get users * @param {String} token the user request token @@ -323,6 +357,39 @@ async function getUserById (token, userId) { return _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) } +/** + * Function to create user in ubhan + * @param {Object} data the user data + * @returns the request result + */ +async function createUbhanUser ({ handle, firstName, lastName }) { + const token = await getM2Mtoken() + const res = await request + .post(`${config.TC_API}/users`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send({ handle, firstName, lastName }) + localLogger.debug({ context: 'createUbhanUser', message: `response body: ${JSON.stringify(res.body)}` }) + return _.pick(res.body, ['id']) +} + +/** + * Function to create external profile for a ubhan user + * @param {String} userId the user id(with uuid format) + * @param {Object} data the profile data + */ +async function createUserExternalProfile (userId, { organizationId, externalId }) { + const token = await getM2Mtoken() + const res = await request + .post(`${config.TC_API}/users/${userId}/externalProfiles`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .send({ organizationId, externalId: String(externalId) }) + localLogger.debug({ context: 'createUserExternalProfile', message: `response body: ${JSON.stringify(res.body)}` }) +} + /** * Function to get members * @param {String} token the user request token @@ -410,7 +477,10 @@ module.exports = { getBusApiClient, isDocumentMissingException, getProjects, + getTopcoderUserById, getUserById, + createUbhanUser, + createUserExternalProfile, getMembers, getProjectById, getSkillById, diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index d768cf84..36fca156 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -15,6 +15,29 @@ const models = require('../models') const JobCandidate = models.JobCandidate const esClient = helper.getESClient() +/** + * Make sure a user exists in ubahn(/v5/users) and return the id of the user. + * + * In the case the user does not exist in /v5/users but can be found in /v3/users + * Fetch the user info from /v3/users and create a new user in /v5/users. + * + * @params {Object} currentUser the user who perform this operation + * @returns {undefined} + */ +async function _ensureUbhanUserId (currentUser) { + try { + return await helper.getUserId(currentUser.userId) + } catch (err) { + if (!(err instanceof errors.NotFoundError)) { + throw err + } + const topcoderUser = await helper.getTopcoderUserById(currentUser.userId) + const user = await helper.createUbhanUser(_.pick(topcoderUser, ['handle', 'firstName', 'lastName'])) + await helper.createUserExternalProfile(user.id, { organizationId: config.ORG_ID, externalId: currentUser.userId }) + return user.id + } +} + /** * Get jobCandidate by id * @param {String} id the jobCandidate id @@ -56,7 +79,7 @@ getJobCandidate.schema = Joi.object().keys({ async function createJobCandidate (currentUser, jobCandidate) { jobCandidate.id = uuid() jobCandidate.createdAt = new Date() - jobCandidate.createdBy = await helper.getUserId(currentUser.userId) + jobCandidate.createdBy = await _ensureUbhanUserId(currentUser) jobCandidate.status = 'open' const created = await JobCandidate.create(jobCandidate) @@ -82,16 +105,17 @@ createJobCandidate.schema = Joi.object().keys({ async function updateJobCandidate (currentUser, id, data) { const jobCandidate = await JobCandidate.findById(id) const projectId = await JobCandidate.getProjectId(jobCandidate.dataValues.jobId) + const userId = await _ensureUbhanUserId(currentUser) if (projectId && !currentUser.isBookingManager) { const connect = await helper.isConnectMember(projectId, currentUser.jwtToken) if (!connect) { - if (jobCandidate.dataValues.userId !== await helper.getUserId(currentUser.userId)) { + if (jobCandidate.dataValues.userId !== userId) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } } } data.updatedAt = new Date() - data.updatedBy = await helper.getUserId(currentUser.userId) + data.updatedBy = userId await jobCandidate.update(data) await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, { id, ...data })