From 8368b32125ace23b52387ec0a4631b12276f053a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 18 May 2022 17:23:19 +1000 Subject: [PATCH 01/16] Drop down selector for Connect project instead of text field https://github.com/topcoder-platform/topcoder-x-ui/issues/443 --- .DS_Store | Bin 10244 -> 10244 bytes configuration.md | 1 + src/config.js | 1 + src/front/src/app/projects/project.service.js | 47 +++++++++++++++++- .../upsertproject/upsertproject.controller.js | 12 +++++ .../src/app/upsertproject/upsertproject.html | 9 ++-- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/.DS_Store b/.DS_Store index 94caba4894ade7ff9895ec61d11d6caabd40eb6b..05d56ce198a2978b87d0b296090082f9a0e82022 100644 GIT binary patch delta 254 zcmZn(XbIS$E*Pt{<1GUN0}F#5LpnnyLrHGFi%U{YeiBfOW4Yz#>*q`kJEF>`;FT}P zFbq!4&n*DzVPMeS+#(pt!o+iUGNYKt=9@0ody~?NlYHboUA6Mz{Fs%*+a~T zktyD0bB%aEt7?GYeULJc&CCqN3`Gpdx%mitp|X<=WF;9FZ1$4v=OD?7$u;5~077w4 AtpET3 delta 254 zcmZn(XbIS$E*Psg<0JzE0}F#5LpnnyLrHGFi%U{YeiBfOW7hZDd55GAJEF>`;FT}P zFbq!4&n*DzVPMeM+#(pt!gNGyGNYKt=TwVY7#r z5hIgh=H?plepc0tm0)Eco0%Dk8HyN^bMq1QLS-i#$VxI!*z6_S&q0zElWW8~0KUOe AWdHyG diff --git a/configuration.md b/configuration.md index 898f242..c805f3e 100644 --- a/configuration.md +++ b/configuration.md @@ -35,6 +35,7 @@ The following config parameters are supported, they are defined in `src/config.j |AWS_CONNECTION_TIMEOUT | The timeout used to check if the app is healthy. |10000 | |TC_LOGIN_URL | TC login url | | |DYNAMODB_WAIT_TABLE_FOR_ACTIVE_TIMEOUT | Dynamodb wait for active timeout |10 minutes | +|TC_API_V5_URL | Topcoder API v5 url for retrieving list of Connect Projects | | ## GitHub OAuth App Setup diff --git a/src/config.js b/src/config.js index 031e70e..4489194 100644 --- a/src/config.js +++ b/src/config.js @@ -83,4 +83,5 @@ module.exports.frontendConfigs = { TOPCODER_URL: process.env.TOPCODER_URL || 'https://topcoder-dev.com', GITHUB_TEAM_URL: process.env.GITHUB_TEAM_URL || 'https://github.com/orgs/', GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || 'https://gitlab.com/groups/', + TC_API_V5_URL: process.env.TC_API_V5_URL || 'https://api.topcoder-dev.com/v5', }; diff --git a/src/front/src/app/projects/project.service.js b/src/front/src/app/projects/project.service.js index 615f5c1..730b939 100644 --- a/src/front/src/app/projects/project.service.js +++ b/src/front/src/app/projects/project.service.js @@ -6,7 +6,7 @@ 'use strict'; angular.module('topcoderX') - .factory('ProjectService', ['Helper', '$http', function (Helper, $http) { + .factory('ProjectService', ['Helper', '$http', '$rootScope', 'AuthService', function (Helper, $http, $rootScope, AuthService) { // object we will return var ProjectService = {}; var projectsDataPromise = {}; @@ -140,5 +140,50 @@ angular.module('topcoderX') }); }; + /** + * Get associated connect projects that the current user has access to + */ + ProjectService.getConnectProjects = function() { + function createProjectRequest(pagingParams) { + return $http({ + method: 'GET', + url: $rootScope.appConfig.TC_API_V5_URL + '/projects/', + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + AuthService.AuthService.getTokenV3() + }, + params: { + fields: 'id,name,status', + sort: 'lastActivityAt desc', + perPage: pagingParams.perPage, + page: pagingParams.page + } + }); + } + + function getAll(getter, perPage, page, prev) { + return getter({ + perPage: perPage, + page: page + }).then(function (res) { + if (res.status === 200) { + var data = res.data; + if (!data.length) return prev || []; + var current = []; + if (prev) { + current = prev.concat(data); + } else { + current = data; + } + return getAll(getter, perPage, 1 + page, current); + } + return prev || []; + }); + } + return getAll(function (params) { return createProjectRequest(params) }, 20, 1).then(function(response) { + return response; + }); + } + return ProjectService; }]); diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index 5ec6b95..c6dc417 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -40,6 +40,18 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc } $scope.isAdminUser = Helper.isAdminUser(currentUser); + $scope.loadingConnectProjects = true; + ProjectService.getConnectProjects().then(function (result) { + $scope.loadingConnectProjects = false; + $scope.connectProjects = result.map(function (p) { + return { + label: 'ID: ' + p.id + ', NAME: ' + p.name + ', STATUS: ' + p.status, + value: p.id + } + }); + }).catch(function (error) { + Alert.error(error.data.message, $scope); + }); // function to add labels to the current project. $scope.addLabels = function () { diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index 6549ec5..2563a07 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -56,10 +56,11 @@

{{title}}



- - The Topcoder Connect Project ID of the project. You can obtain this through the URL to the - project in Topcoder Connect. For example: - "https://connect.topcoder.com/projects/16598" - Enter ID "16598" + + Select the Topcoder Connect Project ID of the project. The above list contains all Topcoder Connect Projects + you have access to. The TC Connect Project ID is required.
From 43ad8eafb403b2e99ce9a09e8e9eaa7d7fce1de0 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 18 May 2022 17:24:36 +1000 Subject: [PATCH 02/16] Deploy for testing --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6dc3004..211c0ce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ workflows: context : org-global filters: branches: - only: [develop, "feature/Auth0-RS256-Token"] + only: [develop, "issue_443"] # Production builds are exectuted only on tagged commits to the # master branch. From 638b3e31631f2b6942ac977cefda210163d55c97 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 18 May 2022 18:06:17 +1000 Subject: [PATCH 03/16] Updates for timeout --- .circleci/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 211c0ce..b262e6c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,9 @@ install_dependency: &install_dependency sudo apt install jq sudo pip install awscli --upgrade sudo pip install docker-compose + no_output_timeout: 30m + + install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | @@ -15,6 +18,9 @@ install_deploysuite: &install_deploysuite cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . + no_output_timeout: 30m + + restore_cache_settings_for_build: &restore_cache_settings_for_build key: docker-node-modules-{{ checksum "package-lock.json" }} From 0ad0c52c18cf2be6ba60269d5b3f2d9464ecbf88 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 19 May 2022 10:17:51 +1000 Subject: [PATCH 04/16] Further timeout test --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b262e6c..1a183da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,9 @@ builddeploy_steps: &builddeploy_steps - run: *install_dependency - run: *install_deploysuite - restore_cache: *restore_cache_settings_for_build - - run: ./build.sh + - run: + command: ./build.sh + no_output_timeout: 30m - save_cache: *save_cache_settings - deploy: name: Running MasterScript. @@ -45,6 +47,7 @@ builddeploy_steps: &builddeploy_steps ./buildenv.sh -e $DEPLOY_ENV -b ${DEPLOY_ENV}-${APPNAME}-deployvar source buildenvvar ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${DEPLOY_ENV}-global-appvar,${DEPLOY_ENV}-${APPNAME}-appvar -i ${APPNAME} + jobs: From 8d73ae2a2154b0f04aaba96b098f3db3d483951a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 19 May 2022 10:19:46 +1000 Subject: [PATCH 05/16] =?UTF-8?q?Let=E2=80=99s=20try=20this?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .circleci/config.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a183da..6b30a91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,6 +20,12 @@ install_deploysuite: &install_deploysuite cp ./../buildscript/awsconfiguration.sh . no_output_timeout: 30m +build_app: &build_app + name: Build the app + command: | + ./build.sh + no_output_timeout: 30m + restore_cache_settings_for_build: &restore_cache_settings_for_build key: docker-node-modules-{{ checksum "package-lock.json" }} @@ -35,9 +41,7 @@ builddeploy_steps: &builddeploy_steps - run: *install_dependency - run: *install_deploysuite - restore_cache: *restore_cache_settings_for_build - - run: - command: ./build.sh - no_output_timeout: 30m + - run: *build_app - save_cache: *save_cache_settings - deploy: name: Running MasterScript. From 70772798ed0e01b2f083774f5565336895045d05 Mon Sep 17 00:00:00 2001 From: gets0ul Date: Fri, 20 May 2022 02:22:21 +0700 Subject: [PATCH 06/16] Use ui-select (https://angular-ui.github.io/ui-select/) as dropdown selector for lazy loading the connect projects. --- package.json | 1 + src/front/src/app/app.js | 1 + src/front/src/app/projects/project.service.js | 53 ++++++------------- .../upsertproject/upsertproject.controller.js | 31 +++++++---- .../src/app/upsertproject/upsertproject.html | 18 +++++-- src/front/src/app/vendor.less | 12 +++++ src/front/src/index.css | 3 +- src/front/src/index.js | 1 + 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 5841435..bfdcca2 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "superagent-promise": "^1.1.0", "typescript": "~2.3.3", "uuid": "^3.3.2", + "ui-select": "~0.19.8", "winston": "^2.3.1", "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.1" }, diff --git a/src/front/src/app/app.js b/src/front/src/app/app.js index fa0b206..31fb36e 100644 --- a/src/front/src/app/app.js +++ b/src/front/src/app/app.js @@ -7,6 +7,7 @@ angular.module('topcoderX', [ 'ngAnimate', 'ngCookies', 'ngTouch', + 'ui.select', 'ngSanitize', 'ngResource', 'ui.router', diff --git a/src/front/src/app/projects/project.service.js b/src/front/src/app/projects/project.service.js index 730b939..be09d68 100644 --- a/src/front/src/app/projects/project.service.js +++ b/src/front/src/app/projects/project.service.js @@ -142,48 +142,25 @@ angular.module('topcoderX') /** * Get associated connect projects that the current user has access to + * @param perPage the items to retrieve per page + * @param page the page index */ - ProjectService.getConnectProjects = function() { - function createProjectRequest(pagingParams) { - return $http({ - method: 'GET', - url: $rootScope.appConfig.TC_API_V5_URL + '/projects/', - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + AuthService.AuthService.getTokenV3() - }, - params: { - fields: 'id,name,status', - sort: 'lastActivityAt desc', - perPage: pagingParams.perPage, - page: pagingParams.page - } - }); - } - - function getAll(getter, perPage, page, prev) { - return getter({ + ProjectService.getConnectProjects = function(perPage, page) { + return $http({ + method: 'GET', + url: $rootScope.appConfig.TC_API_V5_URL + '/projects/', + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + AuthService.getTokenV3() + }, + params: { + fields: 'id,name,status', + sort: 'lastActivityAt desc', perPage: perPage, page: page - }).then(function (res) { - if (res.status === 200) { - var data = res.data; - if (!data.length) return prev || []; - var current = []; - if (prev) { - current = prev.concat(data); - } else { - current = data; - } - return getAll(getter, perPage, 1 + page, current); - } - return prev || []; - }); - } - return getAll(function (params) { return createProjectRequest(params) }, 20, 1).then(function(response) { - return response; + } }); - } + }; return ProjectService; }]); diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index c6dc417..239ac14 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -41,17 +41,28 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.isAdminUser = Helper.isAdminUser(currentUser); $scope.loadingConnectProjects = true; - ProjectService.getConnectProjects().then(function (result) { - $scope.loadingConnectProjects = false; - $scope.connectProjects = result.map(function (p) { - return { - label: 'ID: ' + p.id + ', NAME: ' + p.name + ', STATUS: ' + p.status, - value: p.id - } + $scope.connectProjects = []; + $scope.fetchConnectProjects = function($event) { + if (!$event) { + $scope.page = 1; + $scope.connectProjects = []; + } else { + $event.stopPropagation(); + $event.preventDefault(); + $scope.page++; + } + if ($scope.page === 500) { + $scope.loadingConnectProjects = false; + return; + } + $scope.loadingConnectProjects = true; + ProjectService.getConnectProjects(20, $scope.page).then(function(resp) { + $scope.connectProjects = $scope.connectProjects.concat(resp.data); + })['finally'](function() { + $scope.loadingConnectProjects = false; }); - }).catch(function (error) { - Alert.error(error.data.message, $scope); - }); + }; + $scope.fetchConnectProjects(); // function to add labels to the current project. $scope.addLabels = function () { diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index 2563a07..ac295c3 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -56,9 +56,21 @@

{{title}}



- + + + {{$select.selected.id}} + + + id: {{cp.id}}, name: {{cp.name}}, status: {{cp.status}} +
+ +
+
+
Select the Topcoder Connect Project ID of the project. The above list contains all Topcoder Connect Projects you have access to. The diff --git a/src/front/src/app/vendor.less b/src/front/src/app/vendor.less index a17ba74..85e6993 100644 --- a/src/front/src/app/vendor.less +++ b/src/front/src/app/vendor.less @@ -75,3 +75,15 @@ multiselect .btn-default { overflow-x: visible; min-height: 0.01%; } + +.ui-select-container { + margin: 20px 0px 3px; + + .ui-select-search { + width: 100% !important; + } +} +.ui-select-match.btn-default-focus { + outline: 0; + box-shadow: none; +} diff --git a/src/front/src/index.css b/src/front/src/index.css index 8b80263..9fab00a 100644 --- a/src/front/src/index.css +++ b/src/front/src/index.css @@ -2,4 +2,5 @@ @import url("../../../node_modules/metismenu/dist/metisMenu.css"); @import url("../../../node_modules/footable/css/footable.core.css"); -@import url("../../../node_modules/angularjs-datepicker/dist/angular-datepicker.min.css"); \ No newline at end of file +@import url("../../../node_modules/angularjs-datepicker/dist/angular-datepicker.min.css"); +@import url("../../../node_modules/ui-select/dist/select.min.css"); \ No newline at end of file diff --git a/src/front/src/index.js b/src/front/src/index.js index 95a2e5c..02f3bf5 100644 --- a/src/front/src/index.js +++ b/src/front/src/index.js @@ -30,4 +30,5 @@ require('pace-js'); require('footable'); require('jquery-ui-dist/jquery-ui'); require('angularjs-datepicker'); +require('ui-select'); window.shortid = require('shortid') \ No newline at end of file From cef4344823dcfad7d1ecbfebda68f14a604a5777 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 20 May 2022 15:18:04 +1000 Subject: [PATCH 07/16] Archived projects still using copilot handle https://github.com/topcoder-platform/topcoder-x-ui/issues/442 --- src/common/db-helper.js | 30 +++++++ src/models/Issue.js | 2 + src/services/CopilotPaymentService.js | 6 ++ src/services/IssueService.js | 3 + src/services/ProjectService.js | 120 ++++++++++++++++++++------ 5 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/common/db-helper.js b/src/common/db-helper.js index a839948..39227ce 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -470,6 +470,35 @@ async function queryOneOrganisation(model, organisation) { }); } +/** + * Query one active repository + * @param {String} url the repository url + * @returns {Promise} + */ +async function queryOneRepository(url) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + url, + }) + .all() + .exec((err, repos) => { + if (err) { + return reject(err); + } + if (!repos || repos.length === 0) resolve(null); + if (repos.length > 1) { + let error = `Repository's url is unique in this version. + This Error must be caused by old data in the Repository table. + The old version can only guarrentee that the active Repository's url is unique. + Please migrate the old Repository table.`; + logger.debug(`queryOneRepository. Error. ${error}`); + reject(error); + } + return resolve(repos[0]); + }); + }); +} + /** * Query one active repository * @param {Object} model the dynamoose model @@ -597,6 +626,7 @@ module.exports = { queryOneActiveProject, queryOneActiveProjectWithFilter, queryOneActiveRepository, + queryOneRepository, queryOneOrganisation, queryOneIssue, queryOneUserByType, diff --git a/src/models/Issue.js b/src/models/Issue.js index 509cb76..d836d58 100644 --- a/src/models/Issue.js +++ b/src/models/Issue.js @@ -38,6 +38,7 @@ const schema = new Schema({ repoUrl: { type: String }, + repositoryIdStr: {type: String, required: false}, labels: { type: Array, required: false, @@ -49,6 +50,7 @@ const schema = new Schema({ }, // From topcoder api challengeId: {type: Number, required: false}, + challengeUUID: {type: String, required: false}, projectId: {type: String}, status: {type: String}, assignedAt: {type: Date, required: false}, diff --git a/src/services/CopilotPaymentService.js b/src/services/CopilotPaymentService.js index a04956f..08d0be0 100644 --- a/src/services/CopilotPaymentService.js +++ b/src/services/CopilotPaymentService.js @@ -86,6 +86,9 @@ async function _ensureEditPermissionAndGetInfo(paymentId, topcoderUser) { if (dbPayment.closed === true) { throw new Error('Closed payment can not be updated'); } + if (dbProject.archived) { + throw new errors.ForbiddenError('You can\'t edit this payment in an archived project'); + } return dbPayment; } @@ -203,6 +206,9 @@ async function create(topcoderUser, payment) { if (dbProject.copilot !== topcoderUser.handle && dbProject.owner !== topcoderUser.handle) { throw new errors.ForbiddenError('You do not have permission to edit this payment'); } + if (dbProject.archived) { + throw new errors.ForbiddenError('You can\'t edit this payment in an archived project'); + } payment.username = dbProject.copilot; payment.closed = false; payment.id = helper.generateIdentifier(); diff --git a/src/services/IssueService.js b/src/services/IssueService.js index e2882b6..ae37c54 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -117,6 +117,9 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { ) { throw new errors.ForbiddenError('You don\'t have access on this project'); } + if (dbProject.archived) { + throw new errors.ForbiddenError('You can\'t access on this archived project'); + } return dbProject; } diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index a2a407f..918d2b5 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -82,7 +82,7 @@ async function _validateProjectData(project, repoUrl) { } if (existsInDatabase) { throw new errors.ValidationError(`This repo already has a Topcoder-X project associated with it. - Copilot: ${existsInDatabase.copilot}, Owner: ${existsInDatabase.owner}`) + Repo: ${repoUrl}, Copilot: ${existsInDatabase.copilot}, Owner: ${existsInDatabase.owner}`) } const provider = await helper.getProviderType(repoUrl); const userRole = project.copilot ? project.copilot : project.owner; @@ -112,9 +112,80 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { ) { throw new errors.ForbiddenError('You don\'t have access on this project'); } + if (dbProject.archived) { + throw new errors.ForbiddenError('You can\'t access on this archived project'); + } return dbProject; } +/** + * create Repository as well as adding git label, hook, wiki + * or + * migrate Repository as well as related Issue and CopilotPayment + * @param {String} repoUrl the repository url + * @param {Object} project the new project + * @param {String} currentUser the topcoder current user + * @returns {void} + * @private + */ +async function _createOrMigrateRepository(repoUrl, project, currentUser) { + let oldRepo = await dbHelper.queryOneRepository(repoUrl); + if (oldRepo) { + if (oldRepo.projectId === project.id) { + throw new Error(`This error should never occur: the projectId of the repository to be migrate + will never equal to the new project id`); + } + if (oldRepo.archived === false) { + throw new Error(`Duplicate active repository should be blocked by _validateProjectData, + or a time-sequence cornercase encountered here`); + } + try { + let oldIssues = await models.Issue.query({repoUrl: oldRepo.url}); + let oldCopilotPaymentPromise = oldIssues.filter(issue => issue.challengeUUID) + .map(issue => models.CopilotPayment.query({challengeUUID: issue.challengeUUID}) + .then(payments => { + if (!payments || payments.length === 0) { + /* eslint-disable-next-line no-console */ + console.log(`No CopilotPayment correspond to Issue with challengeUUID ${issue.challengeUUID}. + The corresponding CopilotPayment may have been removed. + Or, there is bug in old version.`); + return null; + } + if (payments.length > 1) { + throw new Error(`Duplicate CopilotPayment correspond to one Issue with challengeUUID ${issue.challengeUUID}. + There must be bug in old version`); + } + return payments[0]; + })); + let oldCopilotPayment = await Promise.all(oldCopilotPaymentPromise).filter(payment => payment); + + await models.Repository.update({id: oldRepo.id}, {projectId: project.id, archived: false}); + await oldIssues.forEach(issue => models.Issue.update({id: issue.id}, {projectId: project.id})); + await oldCopilotPayment.forEach( + payment => models.CopilotPayment.update({id: payment.id}, {project: project.id}) + ); + } + catch (err) { + throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + } + } else { + try { + await dbHelper.create(models.Repository, { + id: helper.generateIdentifier(), + projectId: project.id, + url: repoUrl, + archived: project.archived + }) + await createLabel({projectId: project.id}, currentUser, repoUrl); + await createHook({projectId: project.id}, currentUser, repoUrl); + await addWikiRules({projectId: project.id}, currentUser, repoUrl); + } + catch (err) { + throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + } + } +} + /** * creates project * @param {Object} project the project detail @@ -142,24 +213,16 @@ async function create(project, currentUser) { project.copilot = project.copilot ? project.copilot.toLowerCase() : null; project.id = helper.generateIdentifier(); - const createdProject = await dbHelper.create(models.Project, project); - + // TODO: The following db operation should/could be moved into one transaction for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax - await dbHelper.create(models.Repository, { - id: helper.generateIdentifier(), - projectId: project.id, - url: repoUrl, - archived: project.archived - }) try { - await createLabel({projectId: project.id}, currentUser, repoUrl); - await createHook({projectId: project.id}, currentUser, repoUrl); - await addWikiRules({projectId: project.id}, currentUser, repoUrl); + await _createOrMigrateRepository(repoUrl, project, currentUser); } catch (err) { - throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}`); + throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); } } + const createdProject = await dbHelper.create(models.Project, project); return createdProject; } @@ -178,6 +241,7 @@ async function update(project, currentUser) { for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax await _validateProjectData(project, repoUrl); } + // TODO: remove the useless code-block if (dbProject.archived === 'false' && project.archived === true) { // project archived detected. const result = { @@ -201,22 +265,22 @@ async function update(project, currentUser) { dbProject[item[0]] = item[1]; return item; }); - const oldRepositories = await dbHelper.queryRepositoriesByProjectId(dbProject.id); - const weebhookIds = {}; - for (const repo of oldRepositories) { // eslint-disable-line - if (repo.registeredWebhookId) { - weebhookIds[repo.url] = repo.registeredWebhookId; - } - await dbHelper.removeById(models.Repository, repo.id); - } + + // TODO: move the following logic into one dynamoose transaction + const repoUrl2Repo = await dbHelper.queryRepositoriesByProjectId(dbProject.id) + .map(repo => { return {[repo.url]: repo}; }); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax - await dbHelper.create(models.Repository, { - id: helper.generateIdentifier(), - projectId: dbProject.id, - url: repoUrl, - archived: project.archived, - registeredWebhookId: weebhookIds[repoUrl] - }) + if (repoUrl in repoUrl2Repo) { + await models.Repository.update({id: repoUrl2Repo[repoUrl].id}, {archived: project.archived}); + } else { + try { + await _createOrMigrateRepository(repoUrl, project, currentUser); + } + catch (err) { + throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + } + } } dbProject.updatedAt = new Date(); return await dbHelper.update(models.Project, dbProject.id, dbProject); From 446b2c305bb5ce1d556845b001c0e566ab9f1791 Mon Sep 17 00:00:00 2001 From: 52code Date: Wed, 25 May 2022 13:02:51 +0800 Subject: [PATCH 08/16] https://github.com/topcoder-platform/topcoder-x-ui/issues/448 --- src/common/db-helper.js | 45 +++++++++++++++++++++-- src/services/CopilotPaymentService.js | 4 +-- src/services/IssueService.js | 2 +- src/services/ProjectService.js | 52 +++++++++++---------------- 4 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/common/db-helper.js b/src/common/db-helper.js index 39227ce..fdca6f3 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -179,6 +179,43 @@ async function queryOneIssue(model, repositoryId, number, provider) { }); } +/** + * Get Issue's id and challengeUUID by repoUrl + * @param {String} repoUrl The repo url + * @returns {Promise} + */ +async function queryIssueIdChallengeUUIDByRepoUrl(repoUrl) { + return await new Promise((resolve, reject) => { + models.Issue.scan('repoUrl').eq(repoUrl) + .attributes(['id', 'challengeUUID']) + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + + +/** + * Get CopilotPayment's id by challengeUUID + * @param {String} challengeUUID The challengeUUID + * @returns {Promise} + */ +async function queryPaymentIdByChallengeUUID(challengeUUID) { + return await new Promise((resolve, reject) => { + models.CopilotPayment.scan('challengeUUID').eq(challengeUUID) + .attributes(['id']) + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result.id); + }); + }); +} + /** * Get single data by query parameters * @param {Object} model The dynamoose model to query @@ -257,7 +294,7 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { async function queryOneActiveProject(model, repoUrl) { return await new Promise((resolve, reject) => { queryOneActiveRepository(models.Repository, repoUrl).then((repo) => { - if (!repo) resolve(null); + if (!repo || repo.length === 0) resolve(null); else model.queryOne('id').eq(repo.projectId).consistent() .exec((err, result) => { if (err) { @@ -509,8 +546,8 @@ async function queryOneActiveRepository(model, url) { return await new Promise((resolve, reject) => { model.queryOne({ url, - archived: 'false' }) + .filter('archived').eq('false') .all() .exec((err, result) => { if (err) { @@ -531,8 +568,8 @@ async function queryActiveRepositoriesExcludeByProjectId(url, projectId) { return await new Promise((resolve, reject) => { models.Repository.query({ url, - archived: 'false' }) + .filter('archived').eq('false') .filter('projectId') .not().eq(projectId) .all() @@ -609,6 +646,8 @@ async function populateRepoUrls(projectId) { } module.exports = { + queryIssueIdChallengeUUIDByRepoUrl, + queryPaymentIdByChallengeUUID, getById, getByKey, scan, diff --git a/src/services/CopilotPaymentService.js b/src/services/CopilotPaymentService.js index 08d0be0..317fd08 100644 --- a/src/services/CopilotPaymentService.js +++ b/src/services/CopilotPaymentService.js @@ -86,7 +86,7 @@ async function _ensureEditPermissionAndGetInfo(paymentId, topcoderUser) { if (dbPayment.closed === true) { throw new Error('Closed payment can not be updated'); } - if (dbProject.archived) { + if (dbProject.archived === 'true') { throw new errors.ForbiddenError('You can\'t edit this payment in an archived project'); } return dbPayment; @@ -206,7 +206,7 @@ async function create(topcoderUser, payment) { if (dbProject.copilot !== topcoderUser.handle && dbProject.owner !== topcoderUser.handle) { throw new errors.ForbiddenError('You do not have permission to edit this payment'); } - if (dbProject.archived) { + if (dbProject.archived === 'true') { throw new errors.ForbiddenError('You can\'t edit this payment in an archived project'); } payment.username = dbProject.copilot; diff --git a/src/services/IssueService.js b/src/services/IssueService.js index ae37c54..a9c3fb2 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -117,7 +117,7 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { ) { throw new errors.ForbiddenError('You don\'t have access on this project'); } - if (dbProject.archived) { + if (dbProject.archived === 'true') { throw new errors.ForbiddenError('You can\'t access on this archived project'); } return dbProject; diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index 918d2b5..050d3a8 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -112,7 +112,7 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { ) { throw new errors.ForbiddenError('You don\'t have access on this project'); } - if (dbProject.archived) { + if (dbProject.archived === 'true') { throw new errors.ForbiddenError('You can\'t access on this archived project'); } return dbProject; @@ -140,33 +140,21 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { or a time-sequence cornercase encountered here`); } try { - let oldIssues = await models.Issue.query({repoUrl: oldRepo.url}); - let oldCopilotPaymentPromise = oldIssues.filter(issue => issue.challengeUUID) - .map(issue => models.CopilotPayment.query({challengeUUID: issue.challengeUUID}) - .then(payments => { - if (!payments || payments.length === 0) { - /* eslint-disable-next-line no-console */ - console.log(`No CopilotPayment correspond to Issue with challengeUUID ${issue.challengeUUID}. - The corresponding CopilotPayment may have been removed. - Or, there is bug in old version.`); - return null; - } - if (payments.length > 1) { - throw new Error(`Duplicate CopilotPayment correspond to one Issue with challengeUUID ${issue.challengeUUID}. - There must be bug in old version`); - } - return payments[0]; - })); - let oldCopilotPayment = await Promise.all(oldCopilotPaymentPromise).filter(payment => payment); - - await models.Repository.update({id: oldRepo.id}, {projectId: project.id, archived: false}); - await oldIssues.forEach(issue => models.Issue.update({id: issue.id}, {projectId: project.id})); - await oldCopilotPayment.forEach( - payment => models.CopilotPayment.update({id: payment.id}, {project: project.id}) + const oldIssues = await dbHelper.queryIssueIdChallengeUUIDByRepoUrl(repoUrl); + const issueIds = oldIssues.map(issue => issue.id); + const challengeUUIDs = oldIssues.map(issue => issue.challengeUUID).filter(challengeUUID => challengeUUID); + const paymentIds = await Promise.all( + challengeUUIDs.map(challengeUUID => dbHelper.queryPaymentIdByChallengeUUID(challengeUUID)) + ); + + await dbHelper.update(models.Repository, oldRepo.id, {projectId: project.id, archived: false}); + await Promise.all(issueIds.map(issueId => dbHelper.update(models.Issue, issueId, {projectId: project.id}))); + await Promise.all( + paymentIds.map(paymentId => dbHelper.update(models.CopilotPayment, paymentId, {project: project.id})) ); } catch (err) { - throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err}`); } } else { try { @@ -181,7 +169,7 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { await addWikiRules({projectId: project.id}, currentUser, repoUrl); } catch (err) { - throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}. Internal Error: ${err}`); } } } @@ -213,6 +201,8 @@ async function create(project, currentUser) { project.copilot = project.copilot ? project.copilot.toLowerCase() : null; project.id = helper.generateIdentifier(); + const createdProject = await dbHelper.create(models.Project, project); + // TODO: The following db operation should/could be moved into one transaction for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax try { @@ -222,7 +212,6 @@ async function create(project, currentUser) { throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); } } - const createdProject = await dbHelper.create(models.Project, project); return createdProject; } @@ -267,12 +256,12 @@ async function update(project, currentUser) { }); // TODO: move the following logic into one dynamoose transaction - const repoUrl2Repo = await dbHelper.queryRepositoriesByProjectId(dbProject.id) - .map(repo => { return {[repo.url]: repo}; }); + const repos = await dbHelper.queryRepositoriesByProjectId(dbProject.id); for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax - if (repoUrl in repoUrl2Repo) { - await models.Repository.update({id: repoUrl2Repo[repoUrl].id}, {archived: project.archived}); + if (repos.find(repo => repo.url === repoUrl)) { + const repoId = repos.find(repo => repo.url === repoUrl).id + await dbHelper.update(models.Repository, repoId, {archived: project.archived}); } else { try { await _createOrMigrateRepository(repoUrl, project, currentUser); @@ -319,7 +308,6 @@ async function getAll(query, currentUser) { query.lastKey = parseInt(query.lastKey, 10); } const slicedProjects = _.slice(projects, query.lastKey, query.lastKey + query.perPage); - // console.log(projects); for (const project of slicedProjects) { // eslint-disable-line project.repoUrls = await dbHelper.populateRepoUrls(project.id); } From 131b2364a8b025677816b44dc8bda26f549d73d2 Mon Sep 17 00:00:00 2001 From: gets0ul Date: Wed, 25 May 2022 19:11:42 +0700 Subject: [PATCH 09/16] Refresh owner user/copilot Gitlab access token automatically when needed --- src/common/constants.js | 6 +++--- src/controllers/GitlabController.js | 16 ++++++++++------ src/models/index.js | 2 +- src/services/GitlabService.js | 8 +++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/common/constants.js b/src/common/constants.js index a1b06ed..92c41e7 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -43,10 +43,10 @@ const GITLAB_MAX_PER_PAGE = Number.MAX_SAFE_INTEGER; // the access level can be: 10 - GUEST, 20 - REPORTER, 30 - DEVELOPER, 40 - MASTER, 50 - OWNER const GITLAB_DEFAULT_GROUP_ACCESS_LEVEL = 30; -// The Gitlab access token default expiration in seconds -const GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION = 3600 * 24 * 14; +// The Gitlab access token default expiration in seconds (2 hours expiration) +const GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION = 3600 * 2; -// The Gitlab refresh token time in seconds before expiration +// The Gitlab refresh token time in seconds before expiration (5 minute before expiration) const GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION = 300; const GITHUB_OWNER_CALLBACK_URL = '/api/v1/github/owneruser/callback'; diff --git a/src/controllers/GitlabController.js b/src/controllers/GitlabController.js index 8eba050..5434b11 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -102,7 +102,9 @@ async function listOwnerUserGroups(req) { if (!user || !user.accessToken) { throw new errors.UnauthorizedError('You have not setup for Gitlab.'); } - return await GitlabService.listOwnerUserGroups(user.accessToken, req.query.page, req.query.perPage, req.query.getAll); + const refreshedUser = await GitlabService.refreshGitlabUserAccessToken(user); + return await GitlabService.listOwnerUserGroups(refreshedUser.accessToken, req.query.page, + req.query.perPage, req.query.getAll); } /** @@ -175,7 +177,7 @@ async function addUserToGroupCallback(req, res) { throw new errors.NotFoundError('The owner user is not found or not accessible.'); } - await GitlabService.refreshGitlabUserAccessToken(ownerUser); + const refreshedOwnerUser = await GitlabService.refreshGitlabUserAccessToken(ownerUser); // exchange code to get normal user token const result = await request @@ -195,7 +197,8 @@ async function addUserToGroupCallback(req, res) { const token = result.body.access_token; // get group name - const groupsResult = await GitlabService.listOwnerUserGroups(ownerUser.accessToken, 1, constants.MAX_PER_PAGE, true); + const groupsResult = await GitlabService.listOwnerUserGroups(refreshedOwnerUser.accessToken, 1, + constants.MAX_PER_PAGE, true); const currentGroup = _.find(groupsResult.groups, (item) => { // eslint-disable-line arrow-body-style return item.id.toString() === group.groupId.toString(); }); @@ -203,7 +206,7 @@ async function addUserToGroupCallback(req, res) { // add user to group const gitlabUser = await GitlabService.addGroupMember( group.groupId, - ownerUser.accessToken, + refreshedOwnerUser.accessToken, token, group.accessLevel, group.expiredAt); @@ -265,11 +268,12 @@ async function deleteUsersFromTeam(req, res) { if (!ownerUser) { throw new errors.NotFoundError('The owner user is not found or not accessible.'); } - await GitlabService.refreshGitlabUserAccessToken(ownerUser); + const refreshedOwnerUser = await GitlabService.refreshGitlabUserAccessToken(ownerUser); const userGroupMappings = await dbHelper.scan(UserGroupMapping, {groupId}); // eslint-disable-next-line no-restricted-syntax for (const userGroupMapItem of userGroupMappings) { - await GitlabService.deleteUserFromGitlabGroup(ownerUser.accessToken, groupId, userGroupMapItem.gitlabUserId); + await GitlabService.deleteUserFromGitlabGroup(refreshedOwnerUser.accessToken, groupId, + userGroupMapItem.gitlabUserId); await dbHelper.removeById(UserGroupMapping, userGroupMapItem.id); } } catch (err) { diff --git a/src/models/index.js b/src/models/index.js index 8038663..f05e709 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -18,7 +18,7 @@ if (config.DYNAMODB.AWS_ACCESS_KEY_ID) { dynamoose.AWS.config.update(dynamooseConfig); if (config.DYNAMODB.IS_LOCAL === 'true') { - dynamoose.local(); + dynamoose.local(config.DYNAMODB.URL); } dynamoose.setDefaults({ diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js index 4144407..eec3086 100644 --- a/src/services/GitlabService.js +++ b/src/services/GitlabService.js @@ -262,10 +262,11 @@ getUserIdByUsername.schema = Joi.object().keys({ /** * Refresh the owner user access token if needed * @param {Object} gitlabOwner the gitlab owner + * @returns {Promise} the promise result of owner user with refreshed token */ async function refreshGitlabUserAccessToken(gitlabOwner) { - if (gitlabOwner.accessTokenExpiration && gitlabOwner.accessTokenExpiration.getTime() <= - new Date().getTime() + constants.GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND) { + if (gitlabOwner.accessTokenExpiration && new Date().getTime() > gitlabOwner.accessTokenExpiration.getTime() - + (constants.GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND)) { const refreshTokenResult = await request .post('https://gitlab.com/oauth/token') .query({ @@ -278,12 +279,13 @@ async function refreshGitlabUserAccessToken(gitlabOwner) { .end(); // save user token data const expiresIn = refreshTokenResult.body.expires_in || constants.GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION; - await dbHelper.update(User, gitlabOwner.id, { + return await dbHelper.update(User, gitlabOwner.id, { accessToken: refreshTokenResult.body.access_token, accessTokenExpiration: new Date(new Date().getTime() + expiresIn * MS_PER_SECOND), refreshToken: refreshTokenResult.body.refresh_token, }); } + return gitlabOwner; } refreshGitlabUserAccessToken.schema = Joi.object().keys({ From e29301e5b687c6e3fdaa238d881af595a1d47d41 Mon Sep 17 00:00:00 2001 From: 52code Date: Fri, 27 May 2022 14:52:49 +0800 Subject: [PATCH 10/16] small fix on PR #449 for issue #448 --- src/controllers/GitlabController.js | 2 +- src/services/ProjectService.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/GitlabController.js b/src/controllers/GitlabController.js index 5434b11..8c60f23 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -263,7 +263,7 @@ async function deleteUsersFromTeam(req, res) { // If groupInDB not exists, then just return if (groupInDB) { try { - const ownerUser = await helper.queryOneUserByTypeAndRole(User, + const ownerUser = await dbHelper.queryOneUserByTypeAndRole(User, groupInDB.ownerUsername, constants.USER_TYPES.GITLAB, constants.USER_ROLES.OWNER); if (!ownerUser) { throw new errors.NotFoundError('The owner user is not found or not accessible.'); diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index 050d3a8..bc702c2 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -150,8 +150,11 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { await dbHelper.update(models.Repository, oldRepo.id, {projectId: project.id, archived: false}); await Promise.all(issueIds.map(issueId => dbHelper.update(models.Issue, issueId, {projectId: project.id}))); await Promise.all( - paymentIds.map(paymentId => dbHelper.update(models.CopilotPayment, paymentId, {project: project.id})) + paymentIds.filter(paymentId => paymentId) + .map(paymentId => dbHelper.update(models.CopilotPayment, paymentId, {project: project.id})) ); + + await createHook({projectId: project.id}, currentUser, repoUrl); } catch (err) { throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err}`); From c4e694fce01605efde89de43f4a538d80c83f14e Mon Sep 17 00:00:00 2001 From: gets0ul Date: Tue, 31 May 2022 13:40:10 +0700 Subject: [PATCH 11/16] Changes to Connect ID dropdown: - only show project's name - list only active project - change field label --- src/front/src/app/projects/project.service.js | 20 +++++++++++++++++-- .../upsertproject/upsertproject.controller.js | 17 ++++++++++++++-- .../src/app/upsertproject/upsertproject.html | 12 +++++------ src/front/src/app/vendor.less | 14 +++++++++---- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/front/src/app/projects/project.service.js b/src/front/src/app/projects/project.service.js index be09d68..24a06ef 100644 --- a/src/front/src/app/projects/project.service.js +++ b/src/front/src/app/projects/project.service.js @@ -154,10 +154,26 @@ angular.module('topcoderX') "Authorization": "Bearer " + AuthService.getTokenV3() }, params: { - fields: 'id,name,status', + fields: 'id,name', sort: 'lastActivityAt desc', perPage: perPage, - page: page + page: page, + status: 'active' + } + }); + }; + + /** + * Get connect project by id + * @param id the id + */ + ProjectService.getConnectProject = function(id) { + return $http({ + method: 'GET', + url: $rootScope.appConfig.TC_API_V5_URL + '/projects/' + id, + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + AuthService.getTokenV3() } }); }; diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index 239ac14..9596f60 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -26,6 +26,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc archived: false, createCopilotPayments: false }; + $scope.connectProjects = []; if ($rootScope.project) { $scope.title = 'Manage a Project'; $scope.project = $rootScope.project; @@ -34,6 +35,15 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.project.owner = $rootScope.project.owner; $scope.project.repoUrl = $rootScope.project.repoUrls.join(','); $scope.editing = true; + if ($rootScope.project.tcDirectId) { + ProjectService.getConnectProject($rootScope.project.tcDirectId).then(function (resp) { + var connectProject = { + id: resp.data.id, + name: resp.data.name + }; + $scope.connectProjects.unshift(connectProject); + }); + } } else { $scope.title = 'Add a Project'; $scope.editing = false; @@ -41,7 +51,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.isAdminUser = Helper.isAdminUser(currentUser); $scope.loadingConnectProjects = true; - $scope.connectProjects = []; + $scope.fetchConnectProjects = function($event) { if (!$event) { $scope.page = 1; @@ -57,7 +67,10 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc } $scope.loadingConnectProjects = true; ProjectService.getConnectProjects(20, $scope.page).then(function(resp) { - $scope.connectProjects = $scope.connectProjects.concat(resp.data); + var projects = resp.data.filter(function (p) { + return $rootScope.project && $rootScope.project.tcDirectId ? p.id !== $rootScope.project.tcDirectId : true; + }); + $scope.connectProjects = $scope.connectProjects.concat(projects); })['finally'](function() { $scope.loadingConnectProjects = false; }); diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index ac295c3..848837e 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -55,26 +55,26 @@

{{title}}

project Title is required.

- + - {{$select.selected.id}} + {{$select.selected.name}} - id: {{cp.id}}, name: {{cp.name}}, status: {{cp.status}} +
- Select the Topcoder Connect Project ID of the project. The above list contains all Topcoder Connect Projects + Select the Topcoder Connect Project to be associated with. The above list contains all Topcoder Connect Projects you have access to. The - TC Connect Project ID is required. + TC Connect Project is required.

diff --git a/src/front/src/app/vendor.less b/src/front/src/app/vendor.less index 85e6993..bfbc662 100644 --- a/src/front/src/app/vendor.less +++ b/src/front/src/app/vendor.less @@ -13,24 +13,24 @@ width: 300px; text-align: center; background-color: #fff; - + .logo-header { padding: 13px; border: solid 2px #fff; background-color: #3e3e3e } - + h3 { color: #4a4a4a; font-weight: 200; font-family: 'sofia-pro'; text-transform: uppercase; } - + form { padding: 20px; } - + button { text-transform: uppercase; } @@ -87,3 +87,9 @@ multiselect .btn-default { outline: 0; box-shadow: none; } +.ui-select-bootstrap .ui-select-choices-row.active > span { + background: #23c6c8 !important; +} +.ui-select-bootstrap .ui-select-choices-row.active:last-child > span { + background: linear-gradient(180deg, #23c6c8 40%, transparent 0%) !important; +} From 9d116739423efc56e898117bf902f57362e44139 Mon Sep 17 00:00:00 2001 From: 52code Date: Thu, 2 Jun 2022 11:59:29 +0800 Subject: [PATCH 12/16] https://github.com/topcoder-platform/topcoder-x-ui/issues/453 --- src/config.js | 9 +++ src/front/src/app/projects/project.service.js | 14 ++++ .../upsertproject/upsertproject.controller.js | 11 ++- .../src/app/upsertproject/upsertproject.html | 17 +++++ src/models/Project.js | 5 ++ src/services/ProjectService.js | 67 +++++++++++++++---- 6 files changed, 108 insertions(+), 15 deletions(-) diff --git a/src/config.js b/src/config.js index 4489194..599dc29 100644 --- a/src/config.js +++ b/src/config.js @@ -84,4 +84,13 @@ module.exports.frontendConfigs = { GITHUB_TEAM_URL: process.env.GITHUB_TEAM_URL || 'https://github.com/orgs/', GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || 'https://gitlab.com/groups/', TC_API_V5_URL: process.env.TC_API_V5_URL || 'https://api.topcoder-dev.com/v5', + TC_API_V4_URL: { + dev: { + process.env.TC_API_V4_URL || 'https://api.topcoder-dev.com/v4', + }, + prod: { + process.env.TC_API_V4_URL || 'https://api.topcoder.com/v4', + }, + }, + TOPCODER_ENV: process.env.TOPCODER_ENV || 'dev', }; diff --git a/src/front/src/app/projects/project.service.js b/src/front/src/app/projects/project.service.js index 24a06ef..523ab4b 100644 --- a/src/front/src/app/projects/project.service.js +++ b/src/front/src/app/projects/project.service.js @@ -178,5 +178,19 @@ angular.module('topcoderX') }); }; + /** + * Get technology tags + */ + ProjectService.getTags = function() { + return $http({ + method: 'GET', + url: $rootScope.appConfig.TC_API_V4_URL[$rootScope.appConfig.TOPCODER_ENV] + '/technologies, + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + AuthService.getTokenV3() + } + }); + }; + return ProjectService; }]); diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index 9596f60..d145dd6 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -30,9 +30,6 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc if ($rootScope.project) { $scope.title = 'Manage a Project'; $scope.project = $rootScope.project; - $scope.project.id = $rootScope.project.id; - $scope.project.copilot = $rootScope.project.copilot; - $scope.project.owner = $rootScope.project.owner; $scope.project.repoUrl = $rootScope.project.repoUrls.join(','); $scope.editing = true; if ($rootScope.project.tcDirectId) { @@ -52,6 +49,14 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.isAdminUser = Helper.isAdminUser(currentUser); $scope.loadingConnectProjects = true; + $scope.tags = []; + $scope.fetchTags = function() { + ProjectService.getTags().then(function (resp) { + $scope.tags = resp.data.map(tag => tag.name); + }); + } + $scope.fetchTags(); + $scope.fetchConnectProjects = function($event) { if (!$event) { $scope.page = 1; diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index 848837e..ab6ec5c 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -77,6 +77,23 @@

{{title}}

TC Connect Project is required.

+ + + + + {{$item}} + + + {{tag}} + + + Select the Tags to be associated with. + The + Project/Challenge tags cannot be empty. [PATCH /challenges/:challengeId requires at least one tag] +
+
The URL to the repository on Github or Gitlab. For example: diff --git a/src/models/Project.js b/src/models/Project.js index ea10c7a..06f1608 100644 --- a/src/models/Project.js +++ b/src/models/Project.js @@ -24,6 +24,11 @@ const schema = new Schema({ type: Number, required: true }, + tags: { + type: Array, + required: true, + default: [] + }, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, archived: {type: String, required: true}, diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index bc702c2..13f7e2e 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -31,11 +31,13 @@ const currentUserSchema = Joi.object().keys({ handle: Joi.string().required(), roles: Joi.array().required(), }); -const projectSchema = { +const updateProjectSchema = { project: { id: Joi.string().required(), title: Joi.string().required(), tcDirectId: Joi.number().required(), + //NOTE: `PATCH /challenges/:challengeId` requires the tags not empty + tags: Joi.array().items(Joi.string().required()).min(1).required(), repoUrl: Joi.string().required(), repoUrls: Joi.array().required(), rocketChatWebhook: Joi.string().allow(null), @@ -57,6 +59,8 @@ const createProjectSchema = { project: { title: Joi.string().required(), tcDirectId: Joi.number().required(), + //NOTE: `PATCH /challenges/:challengeId` requires the tags not empty + tags: Joi.array().items(Joi.string().required()).min(1).required(), repoUrl: Joi.string().required(), copilot: Joi.string().allow(null), rocketChatWebhook: Joi.string().allow(null), @@ -125,7 +129,7 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { * @param {String} repoUrl the repository url * @param {Object} project the new project * @param {String} currentUser the topcoder current user - * @returns {void} + * @returns {Array} challengeUUIDs * @private */ async function _createOrMigrateRepository(repoUrl, project, currentUser) { @@ -159,6 +163,9 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { catch (err) { throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err}`); } + + const oldProject = await dbHelper.getById(models.Project, oldRepo.projectId); + return _.isEqual(oldProject.tags, project.tags) ? [] : challengeUUIDs; } else { try { await dbHelper.create(models.Repository, { @@ -175,6 +182,8 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}. Internal Error: ${err}`); } } + + return []; } /** @@ -206,16 +215,32 @@ async function create(project, currentUser) { const createdProject = await dbHelper.create(models.Project, project); + let challengeUUIDsList = []; // TODO: The following db operation should/could be moved into one transaction for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax try { - await _createOrMigrateRepository(repoUrl, project, currentUser); + const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); + if (!_.isEmpty(challengeUUIDs)) { + challengeUUIDsList.append(challengeUUIDs); + } } catch (err) { throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); } } + // NOTE: Will update challenge tags even if the project is created with archived at this step, currently. + if (!_.isEmpty(challengeUUIDsList)) { + const projectTagsUpdatedEvent = { + event: 'challengeTags.update', + data: { + challengeUUIDsList, + tags: project.tags, + }, + }; + await kafka.send(JSON.stringify(projectTagsUpdatedEvent)); + } + return createdProject; } @@ -253,32 +278,50 @@ async function update(project, currentUser) { */ project.owner = dbProject.owner; project.copilot = project.copilot !== undefined ? project.copilot.toLowerCase() : null; - Object.entries(project).map((item) => { - dbProject[item[0]] = item[1]; - return item; - }); // TODO: move the following logic into one dynamoose transaction - const repos = await dbHelper.queryRepositoriesByProjectId(dbProject.id); + const repos = await dbHelper.queryRepositoriesByProjectId(project.id); + let challengeUUIDsList = []; for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax if (repos.find(repo => repo.url === repoUrl)) { const repoId = repos.find(repo => repo.url === repoUrl).id await dbHelper.update(models.Repository, repoId, {archived: project.archived}); + if (!_.isEqual(dbProject.tags, project.tags)) { + // NOTE: delay query of challengeUUIDs into topcoder-x-processor + challengeUUIDsList.append(repoUrl); + } } else { try { - await _createOrMigrateRepository(repoUrl, project, currentUser); + const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); + if (!_.isEmpty(challengeUUIDs)) { + challengeUUIDsList.append(challengeUUIDs); + } } catch (err) { throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); } } } - dbProject.updatedAt = new Date(); - return await dbHelper.update(models.Project, dbProject.id, dbProject); + project.updatedAt = new Date(); + const updatedProject = await dbHelper.update(models.Project, project.id, project); + + // NOTE: Will update challenge tags even if the project is changed to archived at this step, currently. + if (!_.isEmpty(challengeUUIDsList)) { + const projectTagsUpdatedEvent = { + event: 'challengeTags.update', + data: { + challengeUUIDsList, + tags: project.tags, + }, + }; + await kafka.send(JSON.stringify(projectTagsUpdatedEvent)); + } + + return updatedProject; } -update.schema = projectSchema; +update.schema = updateProjectSchema; /** * gets all projects From 03f10ce48a719fbdacc5c46b0ed8fa7612d8e928 Mon Sep 17 00:00:00 2001 From: 52code Date: Thu, 2 Jun 2022 15:26:05 +0800 Subject: [PATCH 13/16] fix lint of PR#455 for Issue453 --- src/config.js | 6 +++--- src/front/src/app/projects/project.service.js | 2 +- src/front/src/app/upsertproject/upsertproject.html | 3 --- src/services/ProjectService.js | 6 +++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/config.js b/src/config.js index 599dc29..0b8725a 100644 --- a/src/config.js +++ b/src/config.js @@ -84,12 +84,12 @@ module.exports.frontendConfigs = { GITHUB_TEAM_URL: process.env.GITHUB_TEAM_URL || 'https://github.com/orgs/', GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || 'https://gitlab.com/groups/', TC_API_V5_URL: process.env.TC_API_V5_URL || 'https://api.topcoder-dev.com/v5', - TC_API_V4_URL: { + TOPCODER_VALUES: { dev: { - process.env.TC_API_V4_URL || 'https://api.topcoder-dev.com/v4', + TC_API_V4_URL: process.env.TC_API_V4_URL || 'https://api.topcoder-dev.com/v4', }, prod: { - process.env.TC_API_V4_URL || 'https://api.topcoder.com/v4', + TC_API_V4_URL: process.env.TC_API_V4_URL || 'https://api.topcoder.com/v4', }, }, TOPCODER_ENV: process.env.TOPCODER_ENV || 'dev', diff --git a/src/front/src/app/projects/project.service.js b/src/front/src/app/projects/project.service.js index 523ab4b..3f8543c 100644 --- a/src/front/src/app/projects/project.service.js +++ b/src/front/src/app/projects/project.service.js @@ -184,7 +184,7 @@ angular.module('topcoderX') ProjectService.getTags = function() { return $http({ method: 'GET', - url: $rootScope.appConfig.TC_API_V4_URL[$rootScope.appConfig.TOPCODER_ENV] + '/technologies, + url: $rootScope.appConfig.TOPCODER_VALUES[$rootScope.appConfig.TOPCODER_ENV].TC_API_V4_URL + '/technologies', headers: { "Content-Type": "application/json", "Authorization": "Bearer " + AuthService.getTokenV3() diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index ab6ec5c..10fb507 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -78,10 +78,7 @@

{{title}}



- - {{$item}} diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index 13f7e2e..fcfc7b3 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -159,13 +159,13 @@ async function _createOrMigrateRepository(repoUrl, project, currentUser) { ); await createHook({projectId: project.id}, currentUser, repoUrl); + + const oldProject = await dbHelper.getById(models.Project, oldRepo.projectId); + return _.isEqual(oldProject.tags, project.tags) ? [] : challengeUUIDs; } catch (err) { throw new Error(`Update ProjectId for Repository, Issue, CopilotPayment failed. Repo ${repoUrl}. Internal Error: ${err}`); } - - const oldProject = await dbHelper.getById(models.Project, oldRepo.projectId); - return _.isEqual(oldProject.tags, project.tags) ? [] : challengeUUIDs; } else { try { await dbHelper.create(models.Repository, { From 2d04d6a41a1bad7f62d799fdc121301eaa11244a Mon Sep 17 00:00:00 2001 From: 52code Date: Thu, 2 Jun 2022 22:49:40 +0800 Subject: [PATCH 14/16] https://github.com/topcoder-platform/topcoder-x-ui/issues/453 --- .../src/app/upsertproject/upsertproject.controller.js | 4 +++- src/models/Project.js | 4 ++-- src/services/ProjectService.js | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index d145dd6..c60c48a 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -30,6 +30,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc if ($rootScope.project) { $scope.title = 'Manage a Project'; $scope.project = $rootScope.project; + $scope.project.tags = $rootScope.project.tags.split(','); $scope.project.repoUrl = $rootScope.project.repoUrls.join(','); $scope.editing = true; if ($rootScope.project.tcDirectId) { @@ -52,7 +53,8 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $scope.tags = []; $scope.fetchTags = function() { ProjectService.getTags().then(function (resp) { - $scope.tags = resp.data.map(tag => tag.name); + const s = new Set(resp.data.result.content.map(function(tag) { return tag.name; })); + $scope.tags = Array.from(s).sort(); }); } $scope.fetchTags(); diff --git a/src/models/Project.js b/src/models/Project.js index 06f1608..5b4440b 100644 --- a/src/models/Project.js +++ b/src/models/Project.js @@ -25,9 +25,9 @@ const schema = new Schema({ required: true }, tags: { - type: Array, + type: String, required: true, - default: [] + default: '' }, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index fcfc7b3..d8b57f4 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -212,6 +212,7 @@ async function create(project, currentUser) { project.secretWebhookKey = guid.raw(); project.copilot = project.copilot ? project.copilot.toLowerCase() : null; project.id = helper.generateIdentifier(); + project.tags = project.tags.join(','); const createdProject = await dbHelper.create(models.Project, project); @@ -221,7 +222,7 @@ async function create(project, currentUser) { try { const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); if (!_.isEmpty(challengeUUIDs)) { - challengeUUIDsList.append(challengeUUIDs); + challengeUUIDsList.push(challengeUUIDs); } } catch (err) { @@ -278,6 +279,7 @@ async function update(project, currentUser) { */ project.owner = dbProject.owner; project.copilot = project.copilot !== undefined ? project.copilot.toLowerCase() : null; + project.tags = project.tags.join(','); // TODO: move the following logic into one dynamoose transaction const repos = await dbHelper.queryRepositoriesByProjectId(project.id); @@ -289,13 +291,13 @@ async function update(project, currentUser) { await dbHelper.update(models.Repository, repoId, {archived: project.archived}); if (!_.isEqual(dbProject.tags, project.tags)) { // NOTE: delay query of challengeUUIDs into topcoder-x-processor - challengeUUIDsList.append(repoUrl); + challengeUUIDsList.push(repoUrl); } } else { try { const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); if (!_.isEmpty(challengeUUIDs)) { - challengeUUIDsList.append(challengeUUIDs); + challengeUUIDsList.push(challengeUUIDs); } } catch (err) { From c334ca374e3b911446fec79ab990ad0ea7ce9509 Mon Sep 17 00:00:00 2001 From: 52code Date: Fri, 3 Jun 2022 08:17:54 +0800 Subject: [PATCH 15/16] fix-empty-tags --- src/front/src/app/upsertproject/upsertproject.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index c60c48a..b75a6aa 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -30,7 +30,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc if ($rootScope.project) { $scope.title = 'Manage a Project'; $scope.project = $rootScope.project; - $scope.project.tags = $rootScope.project.tags.split(','); + $scope.project.tags = !!$rootScope.project.tags ? $rootScope.project.tags.split(',') : []; $scope.project.repoUrl = $rootScope.project.repoUrls.join(','); $scope.editing = true; if ($rootScope.project.tcDirectId) { From 9935ebbcbadc5caa45e8094e80f000398fa177a0 Mon Sep 17 00:00:00 2001 From: 52code Date: Tue, 14 Jun 2022 10:00:13 +0800 Subject: [PATCH 16/16] https://github.com/topcoder-platform/topcoder-x-ui/issues/459 --- src/common/db-helper.js | 22 +++++++++++ src/common/helper.js | 61 ++++++++++++++++++++++++++--- src/controllers/GithubController.js | 8 ++-- src/controllers/GitlabController.js | 14 ++++--- src/services/GithubService.js | 20 +++++++--- src/services/GitlabService.js | 23 +++++++---- src/services/IssueService.js | 2 +- src/services/ProjectService.js | 6 +-- 8 files changed, 125 insertions(+), 31 deletions(-) diff --git a/src/common/db-helper.js b/src/common/db-helper.js index fdca6f3..8d2cdc8 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -285,6 +285,27 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { }); } +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} provider The git provider + * @param {String} gitUsername The git username + * @returns {Promise} + */ +async function queryTCUsernameByGitUsername(model, provider, gitUsername) { + return await new Promise((resolve, reject) => { + model.queryOne(`${provider}Username`).eq(gitUsername) + .all() + .exec((err, result) => { + if (err) { + logger.debug(`queryTCUsernameByGitUsername. Error. ${err}`); + return reject(err); + } + return resolve(result.topcoderUsername); + }); + }); +} + /** * Get single data by query parameters * @param {Object} model The dynamoose model to query @@ -673,6 +694,7 @@ module.exports = { queryOneUserGroupMapping, queryOneUserTeamMapping, queryOneUserMappingByTCUsername, + queryTCUsernameByGitUsername, queryRepositoriesByProjectId, queryRepositoryByProjectIdFilterUrl }; diff --git a/src/common/helper.js b/src/common/helper.js index e80dc1d..ff28756 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -19,6 +19,8 @@ const bcrypt = require('bcryptjs'); const moment = require('moment'); const parseDomain = require('parse-domain'); const config = require('../config'); +const kafka = require('../utils/kafka'); +const models = require('../models'); const logger = require('./logger'); const errors = require('./errors'); const constants = require('./constants'); @@ -120,6 +122,52 @@ function buildController(controller) { }); } +/** + * Convert github api error. + * @param {String} copilotHandle the copilot handle + * @param {String} provider the git provider + */ +async function sendTokenExpiredEvent(copilotHandle, provider) { + const notificationTokenExpiredEvent = { + event: 'notification.tokenExpired', + data: { + copilotHandle, + provider, + }, + }; + await kafka.send(JSON.stringify(notificationTokenExpiredEvent)); +} + +/** + * Convert github api error. + * @param {Error} err the github api error + * @param {String} message the error message + * @param {String} gitUsername the git username + * @returns {Error} converted error + */ +async function convertGitHubErrorAsync(err, message, gitUsername) { + if (err.statusCode === 401 && gitUsername) { // eslint-disable-line no-magic-numbers + const copilotHandle = await dbHelper.queryTCUsernameByGitUsername(models.GithubUserMapping, 'github', gitUsername); + await sendTokenExpiredEvent(copilotHandle, 'Github'); + } + return convertGitHubError(err, message); +} + +/** + * Convert gitlab api error. + * @param {Error} err the gitlab api error + * @param {String} message the error message + * @param {String} gitUsername the git username + * @returns {Error} converted error + */ +async function convertGitLabErrorAsync(err, message, gitUsername) { + if (err.statusCode === 401 && gitUsername) { // eslint-disable-line no-magic-numbers + const copilotHandle = await dbHelper.queryTCUsernameByGitUsername(models.GitlabUserMapping, 'gitlab', gitUsername); + await sendTokenExpiredEvent(copilotHandle, 'Gitlab'); + } + return convertGitLabError(err, message); +} + /** * Convert github api error. * @param {Error} err the github api error @@ -209,24 +257,23 @@ async function getProviderType(repoUrl) { /** * gets the git username of copilot/owner for a project - * @param {Object} models the db models * @param {Object} project the db project detail * @param {String} provider the git provider * @param {Boolean} isCopilot if true, then get copilot, otherwise get owner * @returns {Object} the owner/copilot for the project */ -async function getProjectCopilotOrOwner(models, project, provider, isCopilot) { +async function getProjectCopilotOrOwner(project, provider, isCopilot) { const userMapping = await dbHelper.queryOneUserMappingByTCUsername( - provider === 'github' ? models.GithubUserMapping : models.GitlabUserMapping, + provider === 'github' ? models.GithubUserMapping : models.GitlabUserMapping, isCopilot ? project.copilot : project.owner); - if (!userMapping || - (provider === 'github' && !userMapping.githubUserId) + if (!userMapping || + (provider === 'github' && !userMapping.githubUserId) || (provider === 'gitlab' && !userMapping.gitlabUserId)) { throw new Error(`Couldn't find ${isCopilot ? 'copilot' : 'owner'} username for '${provider}' for this repository.`); } - let user = await dbHelper.queryOneUserByType(models.User, + let user = await dbHelper.queryOneUserByType(models.User, provider === 'github' ? userMapping.githubUsername : // eslint-disable-line no-nested-ternary userMapping.gitlabUsername, provider); @@ -270,6 +317,8 @@ module.exports = { buildController, convertGitHubError, convertGitLabError, + convertGitHubErrorAsync, + convertGitLabErrorAsync, ensureExists, ensureExistsWithKey, generateIdentifier, diff --git a/src/controllers/GithubController.js b/src/controllers/GithubController.js index 2794131..ea573f6 100644 --- a/src/controllers/GithubController.js +++ b/src/controllers/GithubController.js @@ -160,7 +160,7 @@ async function addUserToTeamCallback(req, res) { const token = result.body.access_token; // get team details - const teamDetails = await GithubService.getTeamDetails(team.ownerToken, team.teamId); + const teamDetails = await GithubService.getTeamDetails(team.ownerUsername, team.ownerToken, team.teamId); const organisation = teamDetails.organization.login; // Add member to organisation @@ -173,7 +173,8 @@ async function addUserToTeamCallback(req, res) { // add user to team console.log(`adding ${token} to ${team.teamId} with ${team.ownerToken}`); /* eslint-disable-line no-console */ - const githubUser = await GithubService.addTeamMember(team.teamId, team.ownerToken, token, team.accessLevel); + const githubUser = await GithubService.addTeamMember( + team.ownerUsername, team.teamId, team.ownerToken, token, team.accessLevel); // associate github username with TC username const mapping = await dbHelper.queryOneUserMappingByTCUsername(GithubUserMapping, req.session.tcUsername); @@ -247,7 +248,8 @@ async function deleteUsersFromTeam(req, res) { }); // eslint-disable-next-line no-restricted-syntax for (const userTeamMapItem of userTeamMappings) { - await GithubService.deleteUserFromGithubTeam(token, teamId, githubOrgId, userTeamMapItem.githubUserName); + await GithubService.deleteUserFromGithubTeam( + teamInDB.ownerUsername, token, teamId, githubOrgId, userTeamMapItem.githubUserName); await dbHelper.removeById(UserTeamMapping, userTeamMapItem.id); } } catch (err) { diff --git a/src/controllers/GitlabController.js b/src/controllers/GitlabController.js index 8c60f23..09ea8be 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -99,11 +99,14 @@ async function ownerUserLoginCallback(req, res) { */ async function listOwnerUserGroups(req) { const user = await UserService.getAccessTokenByHandle(req.currentUser.handle, constants.USER_TYPES.GITLAB); + // NOTE: Only user with topcoder-x account can pass this condition. + // Only them will be inserted into `User` table, + // normal user will not be in the `User` table. if (!user || !user.accessToken) { throw new errors.UnauthorizedError('You have not setup for Gitlab.'); } const refreshedUser = await GitlabService.refreshGitlabUserAccessToken(user); - return await GitlabService.listOwnerUserGroups(refreshedUser.accessToken, req.query.page, + return await GitlabService.listOwnerUserGroups(refreshedUser.username, refreshedUser.accessToken, req.query.page, req.query.perPage, req.query.getAll); } @@ -197,14 +200,15 @@ async function addUserToGroupCallback(req, res) { const token = result.body.access_token; // get group name - const groupsResult = await GitlabService.listOwnerUserGroups(refreshedOwnerUser.accessToken, 1, - constants.MAX_PER_PAGE, true); + const groupsResult = await GitlabService.listOwnerUserGroups(refreshedOwnerUser.username, + refreshedOwnerUser.accessToken, 1, constants.MAX_PER_PAGE, true); const currentGroup = _.find(groupsResult.groups, (item) => { // eslint-disable-line arrow-body-style return item.id.toString() === group.groupId.toString(); }); // add user to group const gitlabUser = await GitlabService.addGroupMember( + refreshedOwnerUser.username, group.groupId, refreshedOwnerUser.accessToken, token, @@ -272,8 +276,8 @@ async function deleteUsersFromTeam(req, res) { const userGroupMappings = await dbHelper.scan(UserGroupMapping, {groupId}); // eslint-disable-next-line no-restricted-syntax for (const userGroupMapItem of userGroupMappings) { - await GitlabService.deleteUserFromGitlabGroup(refreshedOwnerUser.accessToken, groupId, - userGroupMapItem.gitlabUserId); + await GitlabService.deleteUserFromGitlabGroup(refreshedOwnerUser.username, + refreshedOwnerUser.accessToken, groupId, userGroupMapItem.gitlabUserId); await dbHelper.removeById(UserGroupMapping, userGroupMapItem.id); } } catch (err) { diff --git a/src/services/GithubService.js b/src/services/GithubService.js index df559c8..3439a91 100644 --- a/src/services/GithubService.js +++ b/src/services/GithubService.js @@ -187,13 +187,14 @@ getTeamRegistrationUrl.schema = Joi.object().keys({ /** * Add team member. + * @param {String} gitUsername the git username * @param {String} teamId the team id * @param {String} ownerUserToken the owner user token * @param {String} normalUserToken the normal user token * @param {String} accessLevel the team's access level * @returns {Promise} the promise result */ -async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLevel) { +async function addTeamMember(gitUsername, teamId, ownerUserToken, normalUserToken, accessLevel) { let username; let id; let state; @@ -220,7 +221,7 @@ async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLeve }).get('true') .isUndefined() .value()) { - throw helper.convertGitHubError(err, 'Failed to add team member'); + throw await helper.convertGitHubErrorAsync(err, 'Failed to add team member', gitUsername); } } // return github username and its state @@ -228,6 +229,7 @@ async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLeve } addTeamMember.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), teamId: Joi.string().required(), ownerUserToken: Joi.string().required(), normalUserToken: Joi.string().required(), @@ -342,12 +344,13 @@ getUserIdByUsername.schema = Joi.object().keys({ /** * Get team detailed data * + * @param {String} gitUsername git username * @param {String} token user owner token * @param {String|Number} teamId team id * * @returns {Object} team object, see https://developer.github.com/v3/teams/#get-team */ -async function getTeamDetails(token, teamId) { +async function getTeamDetails(gitUsername, token, teamId) { const teamIdAsNumber = !_.isNumber(teamId) ? parseInt(teamId, 10) : teamId; let team; @@ -357,13 +360,14 @@ async function getTeamDetails(token, teamId) { team = teamResponse.data; } catch (err) { - throw helper.convertGitHubError(err, `Failed to get team with id '${teamId}'.`); + throw await helper.convertGitHubErrorAsync(err, `Failed to get team with id '${teamId}'.`, gitUsername); } return team; } getTeamDetails.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), teamId: Joi.alternatives().try(Joi.string(), Joi.number()).required(), }); @@ -372,6 +376,7 @@ getTeamDetails.schema = Joi.object().keys({ /** * Get team detailed data * + * @param {String} gitUsername git username * @param {String} token user owner token * @param {String|Number} teamId team id * @param {String|Number} orgId team id @@ -379,7 +384,7 @@ getTeamDetails.schema = Joi.object().keys({ * * @returns {Object} status object, see https://developer.github.com/v3/teams/members/#remove-team-membership */ -async function deleteUserFromGithubTeam(token, teamId, orgId, githubUserName) { +async function deleteUserFromGithubTeam(gitUsername, token, teamId, orgId, githubUserName) { const teamIdAsNumber = !_.isNumber(teamId) ? parseInt(teamId, 10) : teamId; let deleteResult; try { @@ -388,12 +393,15 @@ async function deleteUserFromGithubTeam(token, teamId, orgId, githubUserName) { const deleteGithubUserEndpoint = `/organizations/${orgId}/team/${teamIdAsNumber}/memberships/${githubUserName}`; deleteResult = await team._request('DELETE', deleteGithubUserEndpoint); } catch (err) { - throw helper.convertGitHubError(err, `Failed to delete user '${githubUserName}' from org with orgId '${orgId}' and team id '${teamId}'.`); + throw await helper.convertGitHubErrorAsync( + err, `Failed to delete user '${githubUserName}' from org with orgId '${orgId}' and team id '${teamId}'.`, + githubUserName); } return deleteResult; } deleteUserFromGithubTeam.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), teamId: Joi.alternatives().try(Joi.string(), Joi.number()).required(), orgId: Joi.string().required(), diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js index eec3086..a6c0314 100644 --- a/src/services/GitlabService.js +++ b/src/services/GitlabService.js @@ -92,6 +92,7 @@ ensureOwnerUser.schema = Joi.object().keys({ /** * List groups of owner user. + * @param {String} gitUsername the git username * @param {String} token the token * @param {Number} page the page number (default to be 1). Must be >= 1 * @param {Number} perPage the page size (default to be constants.GITLAB_DEFAULT_PER_PAGE). @@ -99,7 +100,8 @@ ensureOwnerUser.schema = Joi.object().keys({ * @param {Boolean} getAll get all groups * @returns {Promise} the promise result */ -async function listOwnerUserGroups(token, page = 1, perPage = constants.GITLAB_DEFAULT_PER_PAGE, getAll = false) { +async function listOwnerUserGroups(gitUsername, token, page = 1, perPage = constants.GITLAB_DEFAULT_PER_PAGE, + getAll = false) { try { const response = await request .get(`${config.GITLAB_API_BASE_URL}/api/v4/groups`) @@ -127,11 +129,12 @@ async function listOwnerUserGroups(token, page = 1, perPage = constants.GITLAB_D } return result; } catch (err) { - throw helper.convertGitLabError(err, 'Failed to list user groups'); + throw await helper.convertGitLabErrorAsync(err, 'Failed to list user groups', gitUsername); } } listOwnerUserGroups.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), page: Joi.number().integer().min(1).optional(), perPage: Joi.number().integer().min(1).max(constants.GITLAB_MAX_PER_PAGE) @@ -176,6 +179,7 @@ getGroupRegistrationUrl.schema = Joi.object().keys({ /** * Add group member. + * @param {String} gitUsername the git username * @param {String} groupId the group id * @param {String} ownerUserToken the owner user token * @param {String} normalUserToken the normal user token @@ -183,7 +187,7 @@ getGroupRegistrationUrl.schema = Joi.object().keys({ * @param {String} expiredAt the expired at params to define how long user joined teams. can be null * @returns {Promise} the promise result */ -async function addGroupMember(groupId, ownerUserToken, normalUserToken, accessLevel, expiredAt) { +async function addGroupMember(gitUsername, groupId, ownerUserToken, normalUserToken, accessLevel, expiredAt) { // eslint-disable-line max-params let username; let userId; try { @@ -219,14 +223,16 @@ async function addGroupMember(groupId, ownerUserToken, normalUserToken, accessLe if (err instanceof errors.ApiError) { throw err; } - throw helper.convertGitLabError( - err, `Failed to add group member userId=${userId} accessLevel=${accessLevel} expiredAt=${expiredAt}`); + throw await helper.convertGitLabErrorAsync( + err, `Failed to add group member userId=${userId} accessLevel=${accessLevel} expiredAt=${expiredAt}`, + gitUsername); } return {username, id: userId}; } } addGroupMember.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), groupId: Joi.string().required(), ownerUserToken: Joi.string().required(), normalUserToken: Joi.string().required(), @@ -303,11 +309,12 @@ refreshGitlabUserAccessToken.schema = Joi.object().keys({ /** * delete user fromgroup + * @param {String} gitUsername the git username * @param {String} ownerUserToken the gitlab owner token * @param {String} groupId the gitlab group Id * @param {String} userId the normal user id */ -async function deleteUserFromGitlabGroup(ownerUserToken, groupId, userId) { +async function deleteUserFromGitlabGroup(gitUsername, ownerUserToken, groupId, userId) { try { await request .del(`${config.GITLAB_API_BASE_URL}/api/v4/groups/${groupId}/members/${userId}`) @@ -318,12 +325,14 @@ async function deleteUserFromGitlabGroup(ownerUserToken, groupId, userId) { // If a user is not found from gitlab, then ignore the error // eslint-disable-next-line no-magic-numbers if (err.status !== 404) { - throw helper.convertGitLabError(err, `Failed to delete user from group, userId is ${userId}, groupId is ${groupId}.`); + throw await helper.convertGitLabErrorAsync( + err, `Failed to delete user from group, userId is ${userId}, groupId is ${groupId}.`, gitUsername); } } } deleteUserFromGitlabGroup.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), ownerUserToken: Joi.string().required(), groupId: Joi.string().required(), userId: Joi.string().required(), diff --git a/src/services/IssueService.js b/src/services/IssueService.js index a9c3fb2..00fabd4 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -132,7 +132,7 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { async function recreate(issue, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); const provider = await helper.getProviderType(issue.url); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = issue.url.split('/'); const index = 1; const repoName = results[results.length - index]; diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index d8b57f4..2899ac7 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -501,7 +501,7 @@ search.schema = Joi.object().keys({ async function createLabel(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; @@ -576,7 +576,7 @@ async function createHook(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const dbRepo = await dbHelper.queryRepositoryByProjectIdFilterUrl(dbProject.id, repoUrl); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; @@ -702,7 +702,7 @@ createHook.schema = createLabel.schema; async function addWikiRules(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, dbProject.copilot !== undefined); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, dbProject.copilot !== undefined); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index];