diff --git a/README.md b/README.md index 985714e..d858a4e 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ The following config parameters are supported, they are defined in `config/defau | PARTITION | the kafka partition | 0| | KAFKA_OPTIONS | the connection option for kafka | see below about KAFKA options | |TC_DEV_ENV| the flag whether to use topcoder development api or production| false| +| TC_AUTHN_URL | the Topcoder authentication url | https://topcoder-dev.auth0.com/oauth/ro | +| TC_AUTHN_REQUEST_BODY | the Topcoder authentication request body. This makes use of some environment variables: `TC_USERNAME`, `TC_PASSWORD`, `TC_CLIENT_ID`, `CLIENT_V2CONNECTION` | see `default.js` | +| TC_AUTHZ_URL | the Topcoder authorization url | https://api.topcoder-dev.com/v3/authorizations | | NEW_CHALLENGE_TEMPLATE | the body template for new challenge request. You can change the subTrack, reviewTypes, technologies, .. here | see `default.js` | | NEW_CHALLENGE_DURATION_IN_DAYS | the duration of new challenge | 5 | |TC_URL| the base URL of topcoder to get the challenge URL| defaults to `https://www.topcoder-dev.com`| diff --git a/config/default.js b/config/default.js index 33832fe..c1a1e75 100644 --- a/config/default.js +++ b/config/default.js @@ -26,6 +26,19 @@ module.exports = { } }, TC_DEV_ENV: process.env.NODE_ENV === 'production' ? false : true, + TC_AUTHN_URL: process.env.TC_AUTHN_URL || 'https://topcoder-dev.auth0.com/oauth/ro', + TC_AUTHN_REQUEST_BODY: { + username: process.env.TC_USERNAME || 'mess', + password: process.env.TC_PASSWORD || 'appirio123', + client_id: process.env.TC_CLIENT_ID || 'JFDo7HMkf0q2CkVFHojy3zHWafziprhT', + sso: false, + scope: 'openid profile offline_access', + response_type: 'token', + connection: process.env.CLIENT_V2CONNECTION || 'TC-User-Database', + grant_type: 'password', + device: 'Browser' + }, + TC_AUTHZ_URL: process.env.TC_AUTHZ_URL || 'https://api.topcoder-dev.com/v3/authorizations', NEW_CHALLENGE_TEMPLATE: process.env.NEW_CHALLENGE_TEMPLATE || { milestoneId: 1, subTrack: 'FIRST_2_FINISH', diff --git a/configuration.md b/configuration.md index cc1f595..03aa592 100644 --- a/configuration.md +++ b/configuration.md @@ -13,6 +13,9 @@ The following config parameters are supported, they are defined in `config/defau |KAFKA_CLIENT_CERT | The Kafka SSL certificate to use when connecting| Read from kafka_client.cer file, but this can be set as a string like it is on Heroku | |KAFKA_CLIENT_CERT_KEY | The Kafka SSL certificate key to use when connecting| Read from kafka_client.key file, but this can be set as a string like it is on Heroku| |TC_DEV_ENV| the flag whether to use topcoder development api or production| false| +| TC_AUTHN_URL | the Topcoder authentication url | https://topcoder-dev.auth0.com/oauth/ro | +| TC_AUTHN_REQUEST_BODY | the Topcoder authentication request body. This makes use of some environment variables: `TC_USERNAME`, `TC_PASSWORD`, `TC_CLIENT_ID`, `CLIENT_V2CONNECTION` | see `default.js` | +| TC_AUTHZ_URL | the Topcoder authorization url | https://api.topcoder-dev.com/v3/authorizations | | NEW_CHALLENGE_TEMPLATE | the body template for new challenge request. You can change the subTrack, reviewTypes, technologies, .. here | see `default.js` | | NEW_CHALLENGE_DURATION_IN_DAYS | the duration of new challenge | 5 | |TC_URL| the base URL of topcoder to get the challenge URL| defaults to `https://www.topcoder-dev.com`| diff --git a/package.json b/package.json index b5904c2..db1c7a3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ }, "homepage": "https://gitlab.com/luettich/processor#README", "dependencies": { - "@topcoder-platform/topcoder-api-challenges-v4-wrapper": "^1.0.5", "@topcoder-platform/topcoder-api-challenges-v4-wrapper-dev": "^1.0.5", "axios": "^0.19.0", "circular-json": "^0.5.7", @@ -43,6 +42,7 @@ "node-gitlab-api": "^2.2.8", "nodemailer": "^4.6.7", "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.3", + "topcoder-api-challenges": "^1.0.6", "topcoder-api-projects": "^1.0.1", "topcoder-dev-api-projects": "^1.0.1", "topcoder-healthcheck-dropin": "^1.0.3", diff --git a/services/IssueService.js b/services/IssueService.js index 57dc6bc..bae4f1d 100755 --- a/services/IssueService.js +++ b/services/IssueService.js @@ -197,13 +197,13 @@ async function handleIssueAssignment(event, issue, force = false) { dbIssue = await ensureChallengeExists(event, issue); if (!dbIssue) { - const err = errors.internalDependencyError(`Can't find the issue in DB. It's not found or not accessible`); + const err = errors.internalDependencyError('Can\'t find the issue in DB. It\'s not found or not accessible'); // The dbissue is not found, the db is not accessible, or the issue is still in creation process. // Handle it for rescheduling. await eventService.handleEventGracefully(event, issue, err); return; } - + // Handle multiple assignees. TC-X allows only one assignee. if (event.data.issue.assignees && event.data.issue.assignees.length > 1) { const comment = 'Topcoder-X only supports a single assignee on a ticket to avoid issues with payment'; @@ -313,7 +313,7 @@ async function handleIssueUpdate(event, issue) { dbIssue = await ensureChallengeExists(event, issue, false); if (!dbIssue) { - const err = errors.internalDependencyError(`Can't find the issue in DB. It's not found or not accessible`); + const err = errors.internalDependencyError('Can\'t find the issue in DB. It\'s not found or not accessible'); // The dbissue is not found, the db is not accessible, or the issue is still in creation process. // Handle it for rescheduling. await eventService.handleEventGracefully(event, issue, err); @@ -363,9 +363,9 @@ async function handleIssueClose(event, issue) { let dbIssue; try { dbIssue = await ensureChallengeExists(event, issue); - + if (!dbIssue) { - const err = errors.internalDependencyError(`Can't find the issue in DB. It's not found or not accessible`); + const err = errors.internalDependencyError('Can\'t find the issue in DB. It\'s not found or not accessible'); // The dbissue is not found, the db is not accessible, or the issue is still in creation process. // Handle it for rescheduling. await eventService.handleEventGracefully(event, issue, err); @@ -376,11 +376,11 @@ async function handleIssueClose(event, issue) { // if the issue has payment success or payment pending status, we'll ignore this process. if (dbIssue && dbIssue.status === 'challenge_payment_successful') { - logger.debug(`Ignoring close issue processing. The issue has challenge_payment_successful.`); + logger.debug('Ignoring close issue processing. The issue has challenge_payment_successful.'); return; } if (dbIssue && dbIssue.status === 'challenge_payment_pending') { - logger.debug(`Ignoring close issue processing. The issue has challenge_payment_pending.`); + logger.debug('Ignoring close issue processing. The issue has challenge_payment_pending.'); return; } @@ -641,7 +641,7 @@ async function handleIssueLabelUpdated(event, issue) { // Sometimes Github send label updated event before issue created event. // This process will be ignored. The label will be processed (stored) at hanleIssueCreated. if (!dbIssue) { - logger.debug(`DB record not found. Issue label update ignored.`); + logger.debug('DB record not found. Issue label update ignored.'); return; } await dbHelper.update(models.Issue, dbIssue.id, { @@ -662,7 +662,7 @@ async function handleIssueUnAssignment(event, issue) { dbIssue = await ensureChallengeExists(event, issue); if (!dbIssue) { - const err = errors.internalDependencyError(`Can't find the issue in DB. It's not found or not accessible`); + const err = errors.internalDependencyError('Can\'t find the issue in DB. It\'s not found or not accessible'); // The dbissue is not found, the db is not accessible, or the issue is still in creation process. // Handle it for rescheduling. await eventService.handleEventGracefully(event, issue, err); diff --git a/utils/db-helper.js b/utils/db-helper.js index 1d4db26..40fe1c5 100644 --- a/utils/db-helper.js +++ b/utils/db-helper.js @@ -81,7 +81,7 @@ async function updateMany(model, collection) { return reject(err); } - resolve(result); + return resolve(result); }); }); } @@ -142,7 +142,7 @@ async function remove(Model, queryParams) { return reject(err); } - resolve(dbItem); + return resolve(dbItem); }); } }); diff --git a/utils/logger.js b/utils/logger.js index 272f94b..b2c90d8 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -9,8 +9,8 @@ * @version 1.0 */ 'use strict'; -const config = require('config'); const util = require('util'); +const config = require('config'); const _ = require('lodash'); const winston = require('winston'); const getParams = require('get-parameter-names'); diff --git a/utils/topcoder-api-helper.js b/utils/topcoder-api-helper.js index 761e25d..c55b469 100644 --- a/utils/topcoder-api-helper.js +++ b/utils/topcoder-api-helper.js @@ -13,17 +13,18 @@ 'use strict'; const config = require('config'); +const jwtDecode = require('jwt-decode'); const axios = require('axios'); const _ = require('lodash'); const moment = require('moment'); const circularJSON = require('circular-json'); -const m2mAuth = require('tc-core-library-js').auth.m2m; +// 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', 'TOKEN_CACHE_TIME', 'AUTH0_PROXY_SERVER_URL'])); let topcoderApiProjects = require('topcoder-api-projects'); -let topcoderApiChallenges = require('@topcoder-platform/topcoder-api-challenges-v4-wrapper'); +let topcoderApiChallenges = require('topcoder-api-challenges'); const topcoderDevApiProjects = require('topcoder-dev-api-projects'); const topcoderDevApiChallenges = require('@topcoder-platform/topcoder-api-challenges-v4-wrapper-dev'); @@ -37,12 +38,16 @@ if (config.TC_DEV_ENV) { topcoderApiProjects = topcoderDevApiProjects; topcoderApiChallenges = topcoderDevApiChallenges; } + +// Cache the access token +let cachedAccessToken; + // Init the API instances const projectsClient = topcoderApiProjects.ApiClient.instance; const challengesClient = topcoderApiChallenges.ApiClient.instance; -//Timeout increase to 5 minutes -challengesClient.timeout=300000; +// Timeout increase to 5 minutes +challengesClient.timeout = 300000; const bearer = projectsClient.authentications.bearer; bearer.apiKeyPrefix = 'Bearer'; @@ -51,20 +56,66 @@ const projectsApiInstance = new topcoderApiProjects.DefaultApi(); const challengesApiInstance = new topcoderApiChallenges.DefaultApi(); /** - * Function to get M2M token - * @returns {Promise} The promised token + * Authenticate with Topcoder API and get the access token. + * @returns {String} the access token issued by Topcoder + * @private */ -async function getM2Mtoken() { - return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET); +async function getAccessToken() { + // Check the cached access token + if (cachedAccessToken) { + const decoded = jwtDecode(cachedAccessToken); + if (decoded.iat > new Date().getTime()) { + // Still not expired, just use it + return cachedAccessToken; + } + } + + // Authenticate + const v2Response = await axios.post(config.TC_AUTHN_URL, config.TC_AUTHN_REQUEST_BODY); + const v2IdToken = _.get(v2Response, 'data.id_token'); + const v2RefreshToken = _.get(v2Response, 'data.refresh_token'); + + if (!v2IdToken || !v2RefreshToken) { + throw new Error(`cannot authenticate with topcoder: ${config.TC_AUTHN_URL}`); + } + + // Authorize + const v3Response = await axios.post( + config.TC_AUTHZ_URL, { + param: { + externalToken: v2IdToken, + refreshToken: v2RefreshToken + } + }, { + headers: { + authorization: `Bearer ${v2IdToken}` + } + }); + + cachedAccessToken = _.get(v3Response, 'data.result.content.token'); + + if (!cachedAccessToken) { + throw new Error(`cannot authorize with topcoder: ${config.TC_AUTHZ_URL}`); + } + + return cachedAccessToken; } +// /** +// * Function to get M2M token +// * @returns {Promise} The promised token +// */ +// async function getM2Mtoken() { +// return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET); +// } + /** * Create a new project. * @param {String} projectName the project name * @returns {Number} the created project id */ async function createProject(projectName) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); // eslint-disable-next-line new-cap const projectBody = new topcoderApiProjects.ProjectRequestBody.constructFromObject({ projectName @@ -96,7 +147,7 @@ async function createProject(projectName) { * @returns {Number} the created challenge id */ async function createChallenge(challenge) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); const start = new Date(); const startTime = moment(start).toISOString(); const end = moment(start).add(config.NEW_CHALLENGE_DURATION_IN_DAYS, 'days').toISOString(); @@ -137,7 +188,7 @@ async function createChallenge(challenge) { * @param {Object} challenge the challenge to update */ async function updateChallenge(id, challenge) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`Updating challenge ${id} with ${circularJSON.stringify(challenge)}`); // eslint-disable-next-line new-cap const challengeBody = new topcoderApiChallenges.UpdateChallengeBodyParam.constructFromObject({ @@ -175,7 +226,7 @@ async function updateChallenge(id, challenge) { * @param {Number} id the challenge id */ async function activateChallenge(id) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`Activating challenge ${id}`); try { const response = await new Promise((resolve, reject) => { @@ -215,7 +266,7 @@ async function getChallengeById(id) { if (!_.isNumber(id)) { throw new Error('The challenge id must valid number'); } - const apiKey = await getM2Mtoken(); + const apiKey = await getAccessToken(); logger.debug('Getting topcoder challenge details'); try { const response = await axios.get(`${challengesClient.basePath}/challenges/${id}`, { @@ -246,7 +297,7 @@ async function getChallengeById(id) { * @param {Number} winnerId the winner id */ async function closeChallenge(id, winnerId) { - const apiKey = await getM2Mtoken(); + const apiKey = await getAccessToken(); logger.debug(`Closing challenge ${id}`); try { const response = await axios.post(`${challengesClient.basePath}/challenges/${id}/close?winnerId=${winnerId}`, null, { @@ -276,7 +327,7 @@ async function closeChallenge(id, winnerId) { * @returns {Number} the billing account id */ async function getProjectBillingAccountId(id) { - const apiKey = await getM2Mtoken(); + const apiKey = await getAccessToken(); logger.debug(`Getting project billing detail ${id}`); try { const response = await axios.get(`${projectsClient.basePath}/direct/projects/${id}`, { @@ -308,7 +359,7 @@ async function getProjectBillingAccountId(id) { * @returns {Number} the user id */ async function getTopcoderMemberId(handle) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); try { const response = await axios.get(`${projectsClient.basePath}/members/${handle}`); const statusCode = response ? response.status : null; @@ -327,7 +378,7 @@ async function getTopcoderMemberId(handle) { * @param {Object} resource the resource resource to add */ async function addResourceToChallenge(id, resource) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`adding resource to challenge ${id}`); try { const response = await new Promise((resolve, reject) => { @@ -368,7 +419,7 @@ async function getResourcesFromChallenge(id) { if (!_.isNumber(id)) { throw new Error('The challenge id must valid number'); } - const apiKey = await getM2Mtoken(); + const apiKey = await getAccessToken(); logger.debug(`fetch resource from challenge ${id}`); try { const response = await axios.get(`${challengesClient.basePath}/challenges/${id}/resources`, { @@ -413,7 +464,7 @@ async function roleAlreadySet(id, role) { * @param {Object} resource the resource resource to remove */ async function unregisterUserFromChallenge(id) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`removing resource from challenge ${id}`); try { const response = await new Promise((resolve, reject) => { @@ -450,7 +501,7 @@ async function unregisterUserFromChallenge(id) { * @param {Number} id the challenge id */ async function cancelPrivateContent(id) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`Cancelling challenge ${id}`); try { const response = await new Promise((resolve, reject) => { @@ -498,7 +549,7 @@ async function assignUserAsRegistrant(topcoderUserId, challengeId) { * @param {Object} resource the resource resource to remove */ async function removeResourceToChallenge(id, resource) { - bearer.apiKey = await getM2Mtoken(); + bearer.apiKey = await getAccessToken(); logger.debug(`removing resource from challenge ${id}`); try { const response = await new Promise((resolve, reject) => { @@ -528,7 +579,7 @@ async function removeResourceToChallenge(id, resource) { * @returns {Array} the resources of challenge */ async function getChallengeResources(id) { - const apiKey = await getM2Mtoken(); + const apiKey = await getAccessToken(); logger.debug(`getting resource from challenge ${id}`); try { const response = await axios.get(`${challengesClient.basePath}/challenges/${id}/resources`, {