diff --git a/config/default.js b/config/default.js index 632c5ca..e17d3f4 100644 --- a/config/default.js +++ b/config/default.js @@ -74,5 +74,10 @@ module.exports = { ROLE_ID_SUBMITTER: process.env.ROLE_ID_SUBMITTER || '732339e7-8e30-49d7-9198-cccf9451e221', TYPE_ID_TASK: process.env.TYPE_ID_TASK || 'ecd58c69-238f-43a4-a4bb-d172719b9f31', DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '53a307ce-b4b3-4d6f-b9a1-3741a58f77e6', - DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825' + DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825', + GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION: 3600 * 2, + GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION: 300, + GITLAB_CLIENT_ID: process.env.GITLAB_CLIENT_ID, + GITLAB_CLIENT_SECRET: process.env.GITLAB_CLIENT_SECRET, + GITLAB_OWNER_USER_CALLBACK_URL: process.env.GITLAB_OWNER_USER_CALLBACK_URL }; diff --git a/configuration.md b/configuration.md index 5ca0e4f..a8f5ddb 100644 --- a/configuration.md +++ b/configuration.md @@ -16,6 +16,9 @@ The following config parameters are supported, they are defined in `config/defau | 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`| |GITLAB_API_BASE_URL| the URL for gitlab host| defaults to `https://gitlab.com`| +| GITLAB_CLIENT_ID | the GitLab client id | No default - needs to be set up with same value found in topcoder-x-ui | +| GITLAB_CLIENT_SECRET | the GitLab client secret | No default - needs to be set up with same value found in topcoder-x-ui | +| GITLAB_OWNER_USER_CALLBACK_URL | the GitLab callback redirect uri for refreshing copilot token | No default - needs to be set up with same owner user callback value in topcoder-x-ui | |PAID_ISSUE_LABEL|the label name for paid, should be one of the label configured in topcoder x ui|'tcx_Paid'| |FIX_ACCEPTED_ISSUE_LABEL|the label name for fix accepted, should be one of the label configured in topcoder x ui|'tcx_FixAccepted'| |ASSIGNED_ISSUE_LABEL| the label name for assigned, should be one of the label configured in topcoder x ui| 'tcx_Assigned'| diff --git a/index.js b/index.js index ee57023..ef15b99 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,9 @@ process.on('unhandledRejection', (err) => { }); // dump the configuration to logger -const ignoreConfigLog = ['cert', 'key', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET']; +const ignoreConfigLog = ['cert', 'key', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', + 'GITLAB_CLIENT_ID', 'GITLAB_CLIENT_SECRET']; + /** * Print configs to logger * @param {Object} params the config params diff --git a/services/GitlabService.js b/services/GitlabService.js index 542e9bb..559b31c 100644 --- a/services/GitlabService.js +++ b/services/GitlabService.js @@ -16,9 +16,18 @@ const GitlabAPI = require('node-gitlab-api'); const logger = require('../utils/logger'); const errors = require('../utils/errors'); const helper = require('../utils/helper'); +const dbHelper = require('../utils/db-helper'); +const superagent = require('superagent'); +const superagentPromise = require('superagent-promise'); + +const request = superagentPromise(superagent, Promise); +// milliseconds per second +const MS_PER_SECOND = 1000; const copilotUserSchema = Joi.object().keys({ accessToken: Joi.string().required(), + accessTokenExpiration: Joi.date().required(), + refreshToken: Joi.string().required(), userProviderId: Joi.number().required(), topcoderUsername: Joi.string() }).required(); @@ -80,7 +89,8 @@ function _getIssueUrl(repoPath, issueId) { async function createComment(copilot, project, issueId, body) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, body}, createComment.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); try { body = helper.prepareAutomatedComment(body, copilot); await gitlab.projects.issues.notes.create(projectId, issueId, {body}); @@ -107,7 +117,8 @@ createComment.schema = { async function updateIssue(copilot, project, issueId, title) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, title}, updateIssue.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); try { await gitlab.projects.issues.edit(projectId, issueId, {title}); } catch (err) { @@ -133,7 +144,8 @@ updateIssue.schema = { async function assignUser(copilot, project, issueId, userId) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, userId}, assignUser.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); try { const issue = await gitlab.projects.issues.show(projectId, issueId); const oldAssignees = _.without(issue.assignee_ids, userId); @@ -164,7 +176,8 @@ assignUser.schema = { async function removeAssign(copilot, project, issueId, userId) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, userId}, removeAssign.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); await _removeAssignees(gitlab, projectId, issueId, [userId]); logger.debug(`Gitlab user ${userId} is unassigned from issue number ${issueId}`); } @@ -179,7 +192,8 @@ removeAssign.schema = assignUser.schema; */ async function getUsernameById(copilot, userId) { Joi.attempt({copilot, userId}, getUsernameById.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); const user = await gitlab.users.show(userId); return user ? user.username : null; } @@ -197,7 +211,8 @@ getUsernameById.schema = { */ async function getUserIdByLogin(copilot, login) { Joi.attempt({copilot, login}, getUserIdByLogin.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); const user = await gitlab.users.all({username: login}); return user.length ? user[0].id : null; } @@ -220,7 +235,8 @@ getUserIdByLogin.schema = { async function markIssueAsPaid(copilot, project, issueId, challengeUUID, existLabels, winner, createCopilotPayments) { // eslint-disable-line max-params const projectId = project.id; Joi.attempt({copilot, projectId, issueId, challengeUUID, existLabels, winner, createCopilotPayments}, markIssueAsPaid.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); const labels = _(existLabels).filter((i) => i !== config.FIX_ACCEPTED_ISSUE_LABEL) .push(config.FIX_ACCEPTED_ISSUE_LABEL, config.PAID_ISSUE_LABEL).value(); try { @@ -263,7 +279,8 @@ markIssueAsPaid.schema = { async function changeState(copilot, project, issueId, state) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, state}, changeState.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); try { await gitlab.projects.issues.edit(projectId, issueId, {state_event: state}); } catch (err) { @@ -289,7 +306,8 @@ changeState.schema = { async function addLabels(copilot, project, issueId, labels) { const projectId = project.id; Joi.attempt({copilot, projectId, issueId, labels}, addLabels.schema); - const gitlab = await _authenticate(copilot.accessToken); + const refreshedCopilot = await _refreshGitlabUserAccessToken(copilot); + const gitlab = await _authenticate(refreshedCopilot.accessToken); try { await gitlab.projects.issues.edit(projectId, issueId, {labels: _.join(labels, ',')}); } catch (err) { @@ -305,6 +323,35 @@ addLabels.schema = { labels: Joi.array().items(Joi.string()).required() }; +/** + * Refresh the copilot access token if token is needed + * @param {Object} copilot the copilot + * @returns {Promise} the promise result of copilot with refreshed token + */ +async function _refreshGitlabUserAccessToken(copilot) { + if (copilot.accessTokenExpiration && new Date().getTime() > copilot.accessTokenExpiration.getTime() - + (config.GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND)) { + const refreshTokenResult = await request + .post(`${config.GITLAB_API_BASE_URL}/oauth/token`) + .query({ + client_id: config.GITLAB_CLIENT_ID, + client_secret: config.GITLAB_CLIENT_SECRET, + refresh_token: copilot.refreshToken, + grant_type: 'refresh_token', + redirect_uri: config.GITLAB_OWNER_USER_CALLBACK_URL, + }) + .end(); + // save user token data + const expiresIn = refreshTokenResult.body.expires_in || config.GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION; + return await dbHelper.update(User, copilot.id, { + accessToken: refreshTokenResult.body.access_token, + accessTokenExpiration: new Date(new Date().getTime() + expiresIn * MS_PER_SECOND), + refreshToken: refreshTokenResult.body.refresh_token, + }); + } + return copilot; +} + module.exports = { createComment, diff --git a/services/UserService.js b/services/UserService.js index 3b1b342..71379e7 100755 --- a/services/UserService.js +++ b/services/UserService.js @@ -97,6 +97,8 @@ async function getRepositoryCopilotOrOwner(provider, repoFullName) { return { accessToken: user.accessToken, + accessTokenExpiration: user.accessTokenExpiration, + refreshToken: user.refreshToken, userProviderId: user.userProviderId, topcoderUsername: userMapping.topcoderUsername };