diff --git a/.DS_Store b/.DS_Store index 94caba4..05d56ce 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.circleci/config.yml b/.circleci/config.yml index 6dc3004..6b30a91 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,15 @@ install_deploysuite: &install_deploysuite cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . 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" }} @@ -29,7 +41,7 @@ builddeploy_steps: &builddeploy_steps - run: *install_dependency - run: *install_deploysuite - restore_cache: *restore_cache_settings_for_build - - run: ./build.sh + - run: *build_app - save_cache: *save_cache_settings - deploy: name: Running MasterScript. @@ -39,6 +51,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: @@ -66,7 +79,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. 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/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/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/common/db-helper.js b/src/common/db-helper.js index a839948..8d2cdc8 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 @@ -248,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 @@ -257,7 +315,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) { @@ -470,6 +528,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 @@ -480,8 +567,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) { @@ -502,8 +589,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() @@ -580,6 +667,8 @@ async function populateRepoUrls(projectId) { } module.exports = { + queryIssueIdChallengeUUIDByRepoUrl, + queryPaymentIdByChallengeUUID, getById, getByKey, scan, @@ -597,6 +686,7 @@ module.exports = { queryOneActiveProject, queryOneActiveProjectWithFilter, queryOneActiveRepository, + queryOneRepository, queryOneOrganisation, queryOneIssue, queryOneUserByType, @@ -604,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/config.js b/src/config.js index 031e70e..0b8725a 100644 --- a/src/config.js +++ b/src/config.js @@ -83,4 +83,14 @@ 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', + TOPCODER_VALUES: { + dev: { + TC_API_V4_URL: process.env.TC_API_V4_URL || 'https://api.topcoder-dev.com/v4', + }, + prod: { + 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/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 8eba050..09ea8be 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -99,10 +99,15 @@ 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.'); } - 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.username, refreshedUser.accessToken, req.query.page, + req.query.perPage, req.query.getAll); } /** @@ -175,7 +180,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,15 +200,17 @@ 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.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, - ownerUser.accessToken, + refreshedOwnerUser.accessToken, token, group.accessLevel, group.expiredAt); @@ -260,16 +267,17 @@ 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.'); } - 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.username, + refreshedOwnerUser.accessToken, groupId, userGroupMapItem.gitlabUserId); await dbHelper.removeById(UserGroupMapping, userGroupMapItem.id); } } catch (err) { 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 615f5c1..3f8543c 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,57 @@ 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(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', + sort: 'lastActivityAt desc', + perPage: perPage, + 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() + } + }); + }; + + /** + * Get technology tags + */ + ProjectService.getTags = function() { + return $http({ + method: 'GET', + url: $rootScope.appConfig.TOPCODER_VALUES[$rootScope.appConfig.TOPCODER_ENV].TC_API_V4_URL + '/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 5ec6b95..b75a6aa 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -26,20 +26,63 @@ 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; - $scope.project.id = $rootScope.project.id; - $scope.project.copilot = $rootScope.project.copilot; - $scope.project.owner = $rootScope.project.owner; + $scope.project.tags = !!$rootScope.project.tags ? $rootScope.project.tags.split(',') : []; $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; } $scope.isAdminUser = Helper.isAdminUser(currentUser); + $scope.loadingConnectProjects = true; + + $scope.tags = []; + $scope.fetchTags = function() { + ProjectService.getTags().then(function (resp) { + const s = new Set(resp.data.result.content.map(function(tag) { return tag.name; })); + $scope.tags = Array.from(s).sort(); + }); + } + $scope.fetchTags(); + + $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) { + 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; + }); + }; + $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 6549ec5..10fb507 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -55,13 +55,40 @@

{{title}}

project Title is required.

- - - 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.selected.name}} + + + +
+ +
+
+
+ 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. +
+
+ + + + {{$item}} + + + {{tag}} + + + Select the Tags to be associated with. + The + Project/Challenge tags cannot be empty. [PATCH /challenges/:challengeId requires at least one tag]

diff --git a/src/front/src/app/vendor.less b/src/front/src/app/vendor.less index a17ba74..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; } @@ -75,3 +75,21 @@ 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; +} +.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; +} 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 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/models/Project.js b/src/models/Project.js index ea10c7a..5b4440b 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: String, + required: true, + default: '' + }, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, archived: {type: String, required: true}, 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/CopilotPaymentService.js b/src/services/CopilotPaymentService.js index a04956f..317fd08 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 === 'true') { + 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 === 'true') { + 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/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 4144407..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(), @@ -262,10 +268,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 +285,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({ @@ -301,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}`) @@ -316,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 e2882b6..00fabd4 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 === 'true') { + throw new errors.ForbiddenError('You can\'t access on this archived project'); + } return dbProject; } @@ -129,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 a2a407f..2899ac7 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), @@ -82,7 +86,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 +116,76 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { ) { throw new errors.ForbiddenError('You don\'t have access on this project'); } + if (dbProject.archived === 'true') { + 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 {Array} challengeUUIDs + * @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 { + 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.filter(paymentId => paymentId) + .map(paymentId => dbHelper.update(models.CopilotPayment, paymentId, {project: project.id})) + ); + + 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}`); + } + } 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}`); + } + } + + return []; +} + /** * creates project * @param {Object} project the project detail @@ -141,26 +212,36 @@ 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); + 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 - 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); + const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); + if (!_.isEmpty(challengeUUIDs)) { + challengeUUIDsList.push(challengeUUIDs); + } } 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}`); } } + // 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; } @@ -178,6 +259,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 = { @@ -197,32 +279,51 @@ 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; - }); - 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; + project.tags = project.tags.join(','); + + // TODO: move the following logic into one dynamoose transaction + 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.push(repoUrl); + } + } else { + try { + const challengeUUIDs = await _createOrMigrateRepository(repoUrl, project, currentUser); + if (!_.isEmpty(challengeUUIDs)) { + challengeUUIDsList.push(challengeUUIDs); + } + } + catch (err) { + throw new Error(`Create or migrate repository failed. Repo ${repoUrl}. Internal Error: ${err.message}`); + } } - await dbHelper.removeById(models.Repository, repo.id); } - 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] - }) + 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)); } - dbProject.updatedAt = new Date(); - return await dbHelper.update(models.Project, dbProject.id, dbProject); + + return updatedProject; } -update.schema = projectSchema; +update.schema = updateProjectSchema; /** * gets all projects @@ -255,7 +356,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); } @@ -401,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]; @@ -476,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]; @@ -602,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];