diff --git a/TopcoderXDeploy.md b/TopcoderXDeploy.md index b900d65..8ef10d6 100644 --- a/TopcoderXDeploy.md +++ b/TopcoderXDeploy.md @@ -157,7 +157,7 @@ You can do this by clicking your logged in username in the upper right of the To Once you have registered your account, go into `Project Management` and add a new project for either a Gitlab or Github project you have access to. Gitlab is likely easier for testing - you can create a free test project under your own account. -Use Topcoder Connect ID `16665` since this has a valid billing account in the dev environment. +Use Topcoder Connect ID `17249` since this has a valid billing account in the dev environment. Once it's been added, click `Manage` for the project in the list on `Project Management` and click `Add Webhooks`. Once the webhook has been added, you should be able to see it in the Gitlab project under `Settings` --> `Integrations` --> `Webhooks` diff --git a/package.json b/package.json index 9791e86..5841435 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "create-tables": "CREATE_DB=true node scripts/create-update-tables.js", "migrate-user-mapping": "node scripts/migrate-user-mapping.js", "add-organisation": "node scripts/add-organisation.js", - "log-repository-collisions": "node scripts/log-repository-collisions.js" + "log-repository-collisions": "node scripts/log-repository-collisions.js", + "migrate-repo-url": "node scripts/migrate-repo-url.js" }, "dependencies": { "angular": "~1.8.0", diff --git a/scripts/log-repository-collisions.js b/scripts/log-repository-collisions.js index c4c275c..232364d 100644 --- a/scripts/log-repository-collisions.js +++ b/scripts/log-repository-collisions.js @@ -17,19 +17,21 @@ async function main() { archived: 'false' }).consistent().limit(BATCH_SIZE).startAt(previousKey).exec() for (const project of projects) { - // If url was already found colliding go to a next iteration - if (collidingUrls.includes(project.repoUrl)) continue; - const collisions = await models.Project.scan({ - repoUrl: project.repoUrl, - archived: 'false' - }).exec() - // If scan found only this project go to a next interation - if (collisions.length < 2) continue; - logger.info(`Repository ${project.repoUrl} has ${collisions.length} collisions`); - _.forEach(collisions, collision => { - logger.info(`--- ID: ${collision.id}`) - }) - collidingUrls.push(project.repoUrl) + for (const repoUrl of project.repoUrls) { + // If url was already found colliding go to a next iteration + if (collidingUrls.includes(repoUrl)) continue; + const collisions = await models.Project.scan({ + repoUrl: { contains: project.repoUrl }, + archived: 'false' + }).exec() + // If scan found only this project go to a next interation + if (collisions.length < 2) continue; + logger.info(`Repository ${repoUrl} has ${collisions.length} collisions`); + _.forEach(collisions, collision => { + logger.info(`--- ID: ${collision.id}`) + }) + collidingUrls.push(repoUrl) + } } previousKey = projects.lastKey previousSize = projects.scannedCount diff --git a/scripts/migrate-repo-url.js b/scripts/migrate-repo-url.js new file mode 100644 index 0000000..2afbcf5 --- /dev/null +++ b/scripts/migrate-repo-url.js @@ -0,0 +1,45 @@ +const AWS = require('aws-sdk'); +const helper = require('../src/common/helper'); +const dbHelper = require('../src/common/db-helper'); +const Project = require('../src/models').Project; +const Repository = require('../src/models').Repository; + +if (process.env.IS_LOCAL=="true") { + AWS.config.update({ + endpoint: 'http://localhost:8000' + }); +} +var documentClient = new AWS.DynamoDB.DocumentClient(); + +(async () => { + console.log('Migrating...'); + const params = { + TableName: 'Topcoder_X.Project' + }; + + let items; + do { + items = await documentClient.scan(params).promise(); + items.Items.forEach(async (item) => { + console.log(item); + let repoUrls; + if (item.repoUrls) { + repoUrls = item.repoUrls.values; + } + else { + repoUrls = [item.repoUrl]; + } + for (const url of repoUrls) { // eslint-disable-line + console.log(`Creating ${url}`); + await dbHelper.create(Repository, { + id: helper.generateIdentifier(), + projectId: item.id, + url, + archived: item.archived, + registeredWebhookId: item.registeredWebhookId + }); + } + }); + params.ExclusiveStartKey = items.LastEvaluatedKey; + } while(typeof items.LastEvaluatedKey !== 'undefined'); +})(); \ No newline at end of file diff --git a/src/common/db-helper.js b/src/common/db-helper.js index 45b66f3..c82ddae 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -1,4 +1,7 @@ +// eslint-disable-line max-lines +const _ = require('lodash'); const logger = require('./logger'); +const models = require('../models'); /* * Copyright (c) 2018 TopCoder, Inc. All rights reserved. @@ -59,7 +62,7 @@ async function scan(model, scanParams) { model.scan(scanParams).exec((err, result) => { if (err) { logger.error(`DynamoDB scan error ${err}`); - reject(err); + return reject(err); } return resolve(result.count === 0 ? [] : result); @@ -67,6 +70,50 @@ async function scan(model, scanParams) { }); } +/** + * Get data collection by scan parameters with paging + * @param {Object} model The dynamoose model to scan + * @param {String} size The size of result + * @param {String} lastKey The lastKey param + * @returns {Promise} + */ +async function scanAll(model, size, lastKey) { + return await new Promise((resolve, reject) => { + const scanMethod = model.scan({}).limit(size); + if (lastKey) scanMethod.startAt(lastKey); + scanMethod.exec((err, result) => { + if (err) { + logger.error(`DynamoDB scan error ${err}`); + return reject(err); + } + return resolve(result.count === 0 ? [] : result); + }); + }); +} + +/** + * Get data collection by scan with search + * @param {Object} model The dynamoose model to scan + * @param {String} size The size of result + * @param {String} lastKey The lastKey param + * @param {String} containsKey The contains key param + * @param {String} contains The contains value + * @returns {Promise} + */ +async function scanAllWithSearch(model, size, lastKey, containsKey, contains) { + return await new Promise((resolve, reject) => { + const scanMethod = model.scan(containsKey).contains(contains).limit(size); + if (lastKey) scanMethod.startAt(lastKey); + scanMethod.exec((err, result) => { + if (err) { + logger.error(`DynamoDB scan error ${err}`); + return reject(err); + } + return resolve(result.count === 0 ? [] : result); + }); + }); +} + /** * Get single data by query parameters * @param {Object} model The dynamoose model to query @@ -171,16 +218,16 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { */ async function queryOneActiveProject(model, repoUrl) { return await new Promise((resolve, reject) => { - model.query('repoUrl').eq(repoUrl) - .where('archived') - .eq('false') - .all() - .exec((err, result) => { - if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); - return reject(err); - } - return resolve(result.count === 0 ? null : result[0]); + queryOneActiveRepository(models.Repository, repoUrl).then((repo) => { + if (!repo) resolve(null); + else model.queryOne('id').eq(repo.projectId).consistent() + .exec((err, result) => { + if (err) { + logger.debug(`queryOneActiveProject. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); }); }); } @@ -202,7 +249,7 @@ async function queryOneActiveCopilotPayment(model, project, username) { .all() .exec((err, result) => { if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); + logger.debug(`queryOneActiveCopilotPayment. Error. ${err}`); return reject(err); } return resolve(result.count === 0 ? null : result[0]); @@ -225,7 +272,7 @@ async function queryOneUserGroupMapping(model, groupId, gitlabUserId) { .all() .exec((err, result) => { if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); + logger.debug(`queryOneUserGroupMapping. Error. ${err}`); return reject(err); } return resolve(result.count === 0 ? null : result[0]); @@ -251,7 +298,7 @@ async function queryOneUserTeamMapping(model, teamId, githubUserName, githubOrgI .all() .exec((err, result) => { if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); + logger.debug(`queryOneUserTeamMapping. Error. ${err}`); return reject(err); } return resolve(result.count === 0 ? null : result[0]); @@ -268,18 +315,16 @@ async function queryOneUserTeamMapping(model, teamId, githubUserName, githubOrgI */ async function queryOneActiveProjectWithFilter(model, repoUrl, projectIdToFilter) { return await new Promise((resolve, reject) => { - model.query('repoUrl').eq(repoUrl) - .where('archived') - .eq('false') - .filter('id') - .not().eq(projectIdToFilter) - .all() - .exec((err, result) => { - if (err || !result) { - logger.debug(`queryOneActiveProject. Error. ${err}`); - return reject(err); - } - return resolve(result.count === 0 ? null : result[0]); + queryActiveRepositoriesExcludeByProjectId(models.Repository, repoUrl, projectIdToFilter).then((repos) => { + if (!repos || repos.length === 0) resolve(null); + else model.queryOne('id').eq(repos[0].projectId).consistent() + .exec((err, result) => { + if (err) { + logger.debug(`queryOneActiveProjectWithFilter. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); }); }); } @@ -296,7 +341,7 @@ async function create(Model, data) { dbItem.save((err) => { if (err) { logger.error(`DynamoDB create error ${err}`); - reject(err); + return reject(err); } return resolve(dbItem); @@ -387,22 +432,138 @@ async function queryOneOrganisation(model, organisation) { }); } +/** + * Query one active repository + * @param {Object} model the dynamoose model + * @param {String} url the repository url + * @returns {Promise} + */ +async function queryOneActiveRepository(model, url) { + return await new Promise((resolve, reject) => { + model.queryOne({ + url, + archived: 'false' + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query active repository with project id exclude filter. + * @param {String} url the repository url + * @param {String} projectId the project id + * @returns {Promise} + */ +async function queryActiveRepositoriesExcludeByProjectId(url, projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + url, + archived: 'false' + }) + .filter('projectId') + .not().eq(projectId) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query repository by project id. + * @param {String} projectId the project id + * @returns {Promise} + */ +async function queryRepositoriesByProjectId(projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result); + }); + }); +} + +/** + * Query repository by project id with url filter. + * @param {String} projectId the project id + * @param {String} url the repo url + * @returns {Promise} + */ +async function queryRepositoryByProjectIdFilterUrl(projectId, url) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .filter('url') + .eq(url) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(result.count === 0 ? null : result[0]); + }); + }); +} + +/** + * Find array of repo url of given project id + * @param {String} projectId the project id + * @returns {Promise} + */ +async function populateRepoUrls(projectId) { + return await new Promise((resolve, reject) => { + models.Repository.query({ + projectId + }) + .all() + .exec((err, result) => { + if (err) { + return reject(err); + } + return resolve(_.map(result, 'url')); + }); + }); +} + module.exports = { getById, getByKey, scan, + scanAll, + scanAllWithSearch, create, update, removeById, removeUser, + populateRepoUrls, + queryActiveRepositoriesExcludeByProjectId, queryOneActiveCopilotPayment, queryOneActiveProject, queryOneActiveProjectWithFilter, + queryOneActiveRepository, queryOneOrganisation, queryOneIssue, queryOneUserByType, queryOneUserByTypeAndRole, queryOneUserGroupMapping, queryOneUserTeamMapping, - queryOneUserMappingByTCUsername + queryOneUserMappingByTCUsername, + queryRepositoriesByProjectId, + queryRepositoryByProjectIdFilterUrl }; diff --git a/src/controllers/GithubPATsController.js b/src/controllers/GithubPATsController.js new file mode 100644 index 0000000..b87f430 --- /dev/null +++ b/src/controllers/GithubPATsController.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + */ + +/** + * This controller exposes Github PATs endpoints. + * + * @author kevinkid + * @version 1.0 + */ +const helper = require('../common/helper'); +const GithubPATsService = require('../services/GithubPATsService'); + +/** + * searches the pat according to criteria + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function search(req) { + return await GithubPATsService.search(req.query); +} + +/** + * create pat + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function create(req) { + return await GithubPATsService.create(req.body.pat); +} + +/** + * remove pat item + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function remove(req) { + return await GithubPATsService.remove(req.params.id); +} + + +module.exports = { + search, + create, + remove, +}; + +helper.buildController(module.exports); diff --git a/src/controllers/IssueController.js b/src/controllers/IssueController.js index 162873b..09d57d8 100644 --- a/src/controllers/IssueController.js +++ b/src/controllers/IssueController.js @@ -21,16 +21,6 @@ async function search(req) { return await IssueService.search(req.query, req.currentUser.handle); } -/** - * create an issue - * @param {Object} req the request - * @param {Object} res the response - * @returns {Object} the result - */ -async function create(req) { - return await IssueService.create(req.body, req.currentUser); -} - /** * recreate an issue * Remove the related db record. @@ -45,7 +35,6 @@ async function recreate(req) { module.exports = { search, - create, recreate }; diff --git a/src/controllers/ProjectController.js b/src/controllers/ProjectController.js index 81c89cc..e37416e 100644 --- a/src/controllers/ProjectController.js +++ b/src/controllers/ProjectController.js @@ -9,7 +9,9 @@ * @version 1.0 */ const helper = require('../common/helper'); +const dbHelper = require('../common/db-helper'); const ProjectService = require('../services/ProjectService'); +const models = require('../models'); /** * create project @@ -47,7 +49,19 @@ async function getAll(req) { * @returns {Object} the result */ async function createLabel(req) { - return await ProjectService.createLabel(req.body, req.currentUser); + const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + try { + await ProjectService.createLabel(req.body, req.currentUser, repoUrl); + } + catch (err) { + throw new Error(`Adding the labels failed. Repo ${repoUrl}`); + } + } + return { + success: false + }; } /** @@ -57,7 +71,19 @@ async function createLabel(req) { * @returns {Object} the result */ async function createHook(req) { - return await ProjectService.createHook(req.body, req.currentUser); + const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + try { + await ProjectService.createHook(req.body, req.currentUser, repoUrl); + } + catch (err) { + throw new Error(`Adding the webhook failed. Repo ${repoUrl}`); + } + } + return { + success: false + }; } /** @@ -67,7 +93,19 @@ async function createHook(req) { * @returns {Object} the result */ async function addWikiRules(req) { - return await ProjectService.addWikiRules(req.body, req.currentUser); + const dbProject = await helper.ensureExists(models.Project, req.body.projectId, 'Project'); + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + try { + await ProjectService.addWikiRules(req.body, req.currentUser, repoUrl); + } + catch (err) { + throw new Error(`Adding the wiki rules failed. Repo ${repoUrl}`); + } + } + return { + success: false + }; } /** diff --git a/src/controllers/UserController.js b/src/controllers/UserController.js index e964a7e..f5989eb 100644 --- a/src/controllers/UserController.js +++ b/src/controllers/UserController.js @@ -45,10 +45,54 @@ async function getUserToken(req) { return await UserService.getUserToken(req.query.username, req.query.tokenType); } +/** + * searches user mappings according to criteria + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function search(req) { + return await UserService.search(req.query); +} + +/** + * create user mapping + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function create(req) { + return await UserService.create(req.body.userMapping); +} + +/** + * update user mapping + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function update(req) { + return await UserService.update(req.body.userMapping); +} + +/** + * remove user mapping + * @param {Object} req the request + * @param {Object} res the response + * @returns {Object} the result + */ +async function remove(req) { + return await UserService.remove(req.params.username); +} + module.exports = { getUserSetting, revokeUserSetting, getUserToken, + search, + create, + remove, + update, }; helper.buildController(module.exports); diff --git a/src/front/src/app/add-github-pat/add-github-pat-controller.js b/src/front/src/app/add-github-pat/add-github-pat-controller.js new file mode 100644 index 0000000..4767e69 --- /dev/null +++ b/src/front/src/app/add-github-pat/add-github-pat-controller.js @@ -0,0 +1,32 @@ +'use strict'; + +angular.module('topcoderX') + .controller('AddGithubPATController', ['$scope', '$state', 'GithubPATsService', 'Alert', + function ($scope, $state, GithubPATsService, Alert) { + $scope.pat = { + name: '', + owner: '', + personalAccessToken: '' + }; + + // handle error output + function _handleError(error, defaultMsg) { + const errMsg = error.data ? error.data.message : defaultMsg; + Alert.error(errMsg, $scope); + } + + // create/update pat item + $scope.save = function () { + if (!$scope.editing) { + GithubPATsService.create($scope.pat).then(function (response) { + if (response.data.exist) { + Alert.error('Organisation is already exist with a PAT. Please delete first.', $scope); + } + else $state.go('app.githubPATs'); + }).catch(function (error) { + _handleError(error, 'An error occurred while creating PAT.'); + }); + } + }; + } + ]); diff --git a/src/front/src/app/add-github-pat/add-github-pat.html b/src/front/src/app/add-github-pat/add-github-pat.html new file mode 100644 index 0000000..5a7afbb --- /dev/null +++ b/src/front/src/app/add-github-pat/add-github-pat.html @@ -0,0 +1,40 @@ +
+
+
+
+

{{title}}

+
+
+ +
+
+
+
+
+ + + The + Organisation is required. +
+ + + The + Owner Github Username is required. +
+ + + The + PAT is required. +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/front/src/app/add-user-mapping/add-user-mapping-controller.js b/src/front/src/app/add-user-mapping/add-user-mapping-controller.js new file mode 100644 index 0000000..ea1b978 --- /dev/null +++ b/src/front/src/app/add-user-mapping/add-user-mapping-controller.js @@ -0,0 +1,78 @@ +'use strict'; + +angular.module('topcoderX') + .controller('AddUserMappingController', ['$scope', '$state', 'UserMappingsService', 'Alert', '$rootScope', + function ($scope, $state, UserMappingsService, Alert, $rootScope) { + $scope.userMapping = { + topcoderUsername: '', + githubUsername: '', + gitlabUsername: '' + }; + $scope.editing = true; + if ($rootScope.userMapping) { + $scope.title = 'Edit User Mapping'; + $scope.userMapping.topcoderUsername = $rootScope.userMapping.topcoderUsername; + $scope.userMapping.githubUsername = $rootScope.userMapping.githubUsername; + $scope.userMapping.gitlabUsername = $rootScope.userMapping.gitlabUsername; + $scope.editing = true; + } else { + $scope.title = 'Add User Mapping'; + $scope.editing = false; + } + + // handle error output + function _handleError(error, defaultMsg) { + const errMsg = error.data ? error.data.message : defaultMsg; + Alert.error(errMsg, $scope); + } + + // create/update pat item + $scope.save = function () { + if (!$scope.editing) { + if (!$scope.userMapping.githubUsername && !$scope.userMapping.gitlabUsername) { + Alert.error('Cannot create with empty mappings.'); + return; + } + const userMapping = { + topcoderUsername: $scope.userMapping.topcoderUsername + }; + if ($scope.userMapping.githubUsername) { + userMapping.githubUsername = $scope.userMapping.githubUsername; + } + if ($scope.userMapping.gitlabUsername) { + userMapping.gitlabUsername = $scope.userMapping.gitlabUsername; + } + UserMappingsService.create(userMapping).then(function (response) { + if (response.data.exist) { + Alert.error('User Mapping for ' + response.data.provider + ' is already exist.', $scope); + } + else $state.go('app.userMappings'); + }).catch(function (error) { + _handleError(error, 'An error occurred while creating User Mapping.'); + }); + } else { + if (!$scope.userMapping.githubUsername && !$scope.userMapping.gitlabUsername) { + Alert.error('Cannot update with empty mappings.'); + return; + } + const userMapping = { + topcoderUsername: $scope.userMapping.topcoderUsername + }; + if ($scope.userMapping.githubUsername) { + userMapping.githubUsername = $scope.userMapping.githubUsername; + } + if ($scope.userMapping.gitlabUsername) { + userMapping.gitlabUsername = $scope.userMapping.gitlabUsername; + } + UserMappingsService.update(userMapping).then(function (response) { + if (response.data.exist) { + Alert.error('User Mapping for ' + response.data.provider + ' is already exist.', $scope); + } + else $state.go('app.userMappings'); + }).catch(function (error) { + _handleError(error, 'An error occurred while creating User Mapping.'); + }); + } + }; + } + ]); diff --git a/src/front/src/app/add-user-mapping/add-user-mapping.html b/src/front/src/app/add-user-mapping/add-user-mapping.html new file mode 100644 index 0000000..59c7fbb --- /dev/null +++ b/src/front/src/app/add-user-mapping/add-user-mapping.html @@ -0,0 +1,36 @@ +
+
+
+
+

{{title}}

+
+
+ +
+
+
+
+
+ + + The + Topcoder Handle is required. +
+ + +
+ + +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/front/src/app/app.js b/src/front/src/app/app.js index c3e6911..fa0b206 100644 --- a/src/front/src/app/app.js +++ b/src/front/src/app/app.js @@ -95,14 +95,6 @@ angular.module('topcoderX', [ data: { pageTitle: 'Project Management' }, resolve: { auth: authenticate } }) - .state('app.issue', { - url: '/upsertissue', - controller: 'IssueController', - controllerAs: 'vm', - templateUrl: 'app/upsertissue/upsertissue.html', - data: { pageTitle: 'Project Management' }, - resolve: { auth: authenticate } - }) // following code is commented to hide the menu // un comment this when pages are developed // .state('app.challenges', { @@ -162,9 +154,35 @@ angular.module('topcoderX', [ controller: 'AddCopilotPaymentController', controllerAs: 'vm', resolve: { auth: authenticate } + }) + .state('app.githubPATs', { + url: '/github-pats', + templateUrl: 'app/github-pats/github-pats.html', + controller: 'GithubPATsController', + controllerAs: 'vm', + resolve: { auth: authenticate } + }) + .state('app.addPAT', { + url: '/github-pats', + templateUrl: 'app/add-github-pat/add-github-pat.html', + controller: 'AddGithubPATController', + controllerAs: 'vm', + resolve: { auth: authenticate } + }) + .state('app.userMappings', { + url: '/user-mappings', + templateUrl: 'app/user-mappings/user-mappings.html', + controller: 'UserMappingsController', + controllerAs: 'vm', + resolve: { auth: authenticate } + }) + .state('app.addUserMapping', { + url: '/user-mappings', + templateUrl: 'app/add-user-mapping/add-user-mapping.html', + controller: 'AddUserMappingController', + controllerAs: 'vm', + resolve: { auth: authenticate } }); - - $urlRouterProvider.otherwise('/app/main'); $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|blob):/); }]); diff --git a/src/front/src/app/github-pats/github-pats-controller.js b/src/front/src/app/github-pats/github-pats-controller.js new file mode 100644 index 0000000..b584199 --- /dev/null +++ b/src/front/src/app/github-pats/github-pats-controller.js @@ -0,0 +1,156 @@ +'use strict'; + +angular.module('topcoderX') + .controller('GithubPATsController', ['$scope', '$rootScope', '$state', 'GithubPATsService', '$filter', 'Alert', 'Dialog', + function ($scope, $rootScope, $state, GithubPATsService, $filter, Alert, Dialog) { + $scope.title = 'Github Personal Access Token'; + $scope.topcoderUrl = ''; + + $scope.tableConfig = { + pageNumber: 1, + pageSize: 20, + isLoading: false, + sortBy: 'name', + sortDir: 'desc', + totalPages: 1, + initialized: false + }; + + $scope.addPAT = function () { + $state.go('app.addPAT'); + }; + + /** + * gets the pat + */ + $scope.getPATs = function () { + var config = $scope.tableConfig; + config.isLoading = true; + GithubPATsService.search(config.sortBy, config.sortDir, config.pageNumber, config.pageSize) + .then(function (res) { + config.items = res.data.docs; + config.pages = res.data.pages; + config.initialized = true; + config.isLoading = false; + }).catch(function (err) { + config.isLoading = false; + config.initialized = true; + _handleError(err, 'An error occurred while getting the data for.'); + }); + }; + + $scope.getPATs(); + + // handle errors + function _handleError(error, defaultMsg) { + var errMsg = error.data ? error.data.message : defaultMsg; + Alert.error(errMsg, $scope); + } + + /** + * delete a pat item byId + * @param {Number} id pat id + */ + function _handleDeletePAT(id) { + GithubPATsService.delete(id).then(function () { + Alert.info('Successfully deleted PAT.', $scope); + $rootScope.dialog = null; + $scope.getPATs(); + }).catch(function (er) { + _handleError(er, 'Error deleting pat.'); + }); + } + + $scope.deletePAT = function (pat) { + $rootScope.dialog = { + patId: pat.id, + proceed: false, + }; + + // $log.warn(watcher, $scope); + $scope.$on('dialog.finished', function (event, args) { + if (args.proceed) { + _handleDeletePAT($rootScope.dialog.patId); + } else { + $rootScope.dialog = {}; + } + }); + Dialog.show('Are you sure you want to delete this PAT?', $scope); + }; + + /** + * handles the sort click + * @param criteria the criteria + */ + $scope.sort = function (criteria) { + if (criteria === $scope.tableConfig.sortBy) { + if ($scope.tableConfig.sortDir === 'asc') { + $scope.tableConfig.sortDir = 'desc'; + } else { + $scope.tableConfig.sortDir = 'asc'; + } + } else { + $scope.tableConfig.sortDir = 'asc'; + } + $scope.tableConfig.sortBy = criteria; + $scope.tableConfig.pageNumber = 1; + $scope.getPATs(); + }; + + /** + * handles the change page click + * @param {Number} pageNumber the page number + */ + $scope.changePage = function (pageNumber) { + if (pageNumber === 0 || pageNumber > $scope.tableConfig.pages || + (pageNumber === $scope.tableConfig.pages && + $scope.tableConfig.pageNumber === pageNumber)) { + return false; + } + $scope.tableConfig.pageNumber = pageNumber; + $scope.getPATs(); + }; + + /** + * handles the tab change click + */ + $scope.tabChanged = function () { + $scope.tableConfig.sortBy = 'project'; + $scope.tableConfig.sortDir = 'desc'; + $scope.tableConfig.pageNumber = 1; + $scope.tableConfig.initialized = false; + $scope.getPATs(); + }; + + /** + * get the number array that shows the pagination bar + */ + $scope.getPageArray = function () { + var res = []; + + var pageNo = $scope.tableConfig.pageNumber; + var i = pageNo - 5; + for (i; i <= pageNo; i++) { + if (i > 0) { + res.push(i); + } + } + var j = pageNo + 1; + for (j; j <= $scope.tableConfig.pages && j <= pageNo + 5; j++) { + res.push(j); + } + return res; + }; + + function onInit() { + const domain = window.location.origin; + if (domain.includes('.topcoder-dev.com')) { + $scope.topcoderUrl = 'https://topcoder-dev.com'; + } else { + $scope.topcoderUrl = 'https://topcoder.com'; + } + } + + onInit(); + } + ]); diff --git a/src/front/src/app/github-pats/github-pats.html b/src/front/src/app/github-pats/github-pats.html new file mode 100644 index 0000000..7b3b54f --- /dev/null +++ b/src/front/src/app/github-pats/github-pats.html @@ -0,0 +1,95 @@ +
+
+
+
+
+

Github Personal Access Token

+
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+

You don't have any PATs, Please + +

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ Organisation + + + Owner Github Username + + Actions
{{pat.name}}{{pat.owner}} + +
+ +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/front/src/app/github-pats/github-pats.service.js b/src/front/src/app/github-pats/github-pats.service.js new file mode 100644 index 0000000..5ed244c --- /dev/null +++ b/src/front/src/app/github-pats/github-pats.service.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + * + * This is a service to access the backend api. + */ +'use strict'; + +angular.module('topcoderX') + .factory('GithubPATsService', ['$http', 'Helper', function ($http, Helper) { + var baseUrl = Helper.baseUrl; + var service = {}; + + /** + * searches PATitems + * @param {String} sortBy the sort by + * @param {String} sortDir the sort direction + * @param {Number} pageNo the page number + * @param {Number} pageSize the page size + */ + service.search = function (sortBy, sortDir, pageNo, pageSize) { + return $http.get(baseUrl + '/api/v1/github/pat?sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize) + .then(function (response) { + return response; + }); + }; + + /** + * create a new PAT item + * + */ + service.create = function (bodyParam) { + return $http.post(baseUrl + '/api/v1/github/pat/', { pat: bodyParam }).then(function (response) { + return response; + }); + }; + + /** + * remove PAT item + * + */ + service.delete = function (id) { + return $http.delete(baseUrl + '/api/v1/github/pat/' + id).then(function (response) { + return response.data; + }); + }; + + return service; + }]); diff --git a/src/front/src/app/main/main.html b/src/front/src/app/main/main.html index 3ac1be5..8c1c87e 100644 --- a/src/front/src/app/main/main.html +++ b/src/front/src/app/main/main.html @@ -54,7 +54,7 @@

Dashboard

{{item.assignedAt|hourSince}} - {{item.number}} + {{item.number}} @@ -128,7 +128,7 @@

Dashboard

- {{item.number}} + {{item.number}} @@ -195,7 +195,7 @@

Dashboard

{{item.assignee}} {{item.projectId.title}} - {{item.number}} + {{item.number}} @@ -261,7 +261,7 @@

Dashboard

{{item.assignee}} {{item.projectId.title}} - {{item.number}} + {{item.number}} diff --git a/src/front/src/app/projects/projects.controller.js b/src/front/src/app/projects/projects.controller.js index 7d111f0..b833f0c 100644 --- a/src/front/src/app/projects/projects.controller.js +++ b/src/front/src/app/projects/projects.controller.js @@ -26,11 +26,6 @@ angular.module('topcoderX') $state.go('app.project'); }; - //go to a add issue page - $scope.goIssue = function () { - $state.go('app.issue'); - }; - //the actived project list $scope.projects = []; //the archived project list diff --git a/src/front/src/app/projects/projects.html b/src/front/src/app/projects/projects.html index b960ae2..e38c85e 100644 --- a/src/front/src/app/projects/projects.html +++ b/src/front/src/app/projects/projects.html @@ -74,8 +74,8 @@

You don't have active projects right now. Please target="_blank">{{project.tcDirectId}} - - {{repoType(project.repoUrl)}} + + {{repoType(repoUrl)}}  {{project.owner}} {{project.copilot}} diff --git a/src/front/src/app/upsertissue/upsertissue.controller.js b/src/front/src/app/upsertissue/upsertissue.controller.js deleted file mode 100644 index 1e37924..0000000 --- a/src/front/src/app/upsertissue/upsertissue.controller.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2018 TopCoder, Inc. All rights reserved. - * - * This is the upsertproject controller. - */ -'use strict'; - -angular.module('topcoderX').controller('IssueController', ['currentUser', '$scope', '$timeout', 'ProjectService', 'IssueService', - '$rootScope', '$state', 'Alert', '$log', - function (currentUser, $scope, $timeout, ProjectService, IssueService, $rootScope, $state, - Alert, $log) { - // Maintain the navigation state. - $timeout(function () { - angular.element('#projectsManagement').addClass('active'); - }, 0); - - // get topcoderx projects - $scope.getProjects = function () { - $log.log('getProjects dipanggil cui... dari upsert'); - - ProjectService.getProjects('active', false).then(function (response) { - $scope.projects = response.data; - if ($scope.projects.length === 0) { - _handleError({}, 'There are no projects in Topcoder-X. Please Create a project first.'); - } - }).catch(function (error) { - _handleError(error, 'There are no projects in Topcoder-X. Please Create a project first.'); - }); - }; - $scope.getProjects(); - - // handle error output - function _handleError(error, defaultMsg) { - const errMsg = error.data ? error.data.message : defaultMsg; - Alert.error(errMsg, $scope); - } - - $scope.selectedProject = null; - $scope.issue = { - projectId: '', - prize: null, - title: '', - comment: '', - repoUrl: '' - }; - $scope.title = 'Add an Issue'; - - // save the issue info. - $scope.save = function () { - const selectedProject = angular.fromJson($scope.selectedProject); - $scope.issue.projectId = selectedProject.id; - $scope.issue.repoUrl = selectedProject.repoUrl; - - IssueService.create($scope.issue).then(function (response) { - $scope.selectedProject = null; - $scope.issue = { - projectId: '', - prize: null, - title: '', - comment: '', - repoUrl: '' - }; - Alert.info('Issue #' + response.data.number + ' has been created', $scope); - }).catch(function (error) { - Alert.error(error.data.message, $scope); - }); - }; - }]); diff --git a/src/front/src/app/upsertissue/upsertissue.html b/src/front/src/app/upsertissue/upsertissue.html deleted file mode 100644 index df05fa0..0000000 --- a/src/front/src/app/upsertissue/upsertissue.html +++ /dev/null @@ -1,53 +0,0 @@ -
-
-
-
-

{{title}}

-
-
- -
-
-
-
-
- - - The - project is required. -
-
- - - The price amount of the issue - The - price is required. -
-
- - - The title of the issue - The - title is required. -
-
- - - The content text of the issue -
-
- - -
-
-
-
-
-
diff --git a/src/front/src/app/upsertproject/upsertproject.controller.js b/src/front/src/app/upsertproject/upsertproject.controller.js index afa7865..5ec6b95 100644 --- a/src/front/src/app/upsertproject/upsertproject.controller.js +++ b/src/front/src/app/upsertproject/upsertproject.controller.js @@ -32,6 +32,7 @@ angular.module('topcoderX').controller('ProjectController', ['currentUser', '$sc $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; } else { $scope.title = 'Add a Project'; diff --git a/src/front/src/app/upsertproject/upsertproject.html b/src/front/src/app/upsertproject/upsertproject.html index 72731b2..6549ec5 100644 --- a/src/front/src/app/upsertproject/upsertproject.html +++ b/src/front/src/app/upsertproject/upsertproject.html @@ -64,10 +64,11 @@

{{title}}

TC Connect Project ID is required.

- + The URL to the repository on Github or Gitlab. For example: - "https://github.com/topcoder-platform/topcoder-x-receiver" + "https://github.com/topcoder-platform/topcoder-x-receiver". + Note that you can comma-separate multiple repositories in this text field. The TC Repo URL is required.
diff --git a/src/front/src/app/user-mappings/user-mappings-controller.js b/src/front/src/app/user-mappings/user-mappings-controller.js new file mode 100644 index 0000000..0524e86 --- /dev/null +++ b/src/front/src/app/user-mappings/user-mappings-controller.js @@ -0,0 +1,174 @@ +'use strict'; + +angular.module('topcoderX') + .controller('UserMappingsController', ['$scope', '$rootScope', '$state', 'UserMappingsService', '$filter', 'Alert', 'Dialog', + function ($scope, $rootScope, $state, UserMappingsService, $filter, Alert, Dialog) { + $scope.title = 'Github Personal Access Token'; + $scope.topcoderUrl = ''; + + $scope.tableConfig = { + pageNumber: 1, + pageSize: 20, + isLoading: false, + sortBy: 'topcoderUsername', + sortDir: 'asc', + totalPages: 1, + initialized: false, + query: '', + lastKey: [] + }; + + $scope.addUserMapping = function () { + $rootScope.userMapping = null; + $state.go('app.addUserMapping'); + }; + + $scope.editUserMapping = function (userMapping) { + if (userMapping) { + $rootScope.userMapping = userMapping; + } else { + $rootScope.userMapping = userMapping; + } + $state.go('app.addUserMapping'); + }; + + /** + * gets the user mappings + */ + $scope.getUserMappings = function () { + var config = $scope.tableConfig; + config.isLoading = true; + UserMappingsService.search(config.query, config.sortBy, config.sortDir, config.pageNumber, config.pageSize, config.lastKey[config.pageNumber]) // eslint-disable-line max-len + .then(function (res) { + config.items = res.data.docs; + if (res.data.lastKey && (res.data.lastKey.githubLastKey || res.data.lastKey.gitlabLastKey)) { + config.lastKey[config.pageNumber + 1] = res.data.lastKey; + if (!config.pages || config.pages <= config.pageNumber) { + config.pages = config.pageNumber + 1; + } + } + config.initialized = true; + config.isLoading = false; + }).catch(function (err) { + config.isLoading = false; + config.initialized = true; + _handleError(err, 'An error occurred while getting the data for.'); + }); + }; + + $scope.getUserMappings(); + + // handle errors + function _handleError(error, defaultMsg) { + var errMsg = error.data ? error.data.message : defaultMsg; + Alert.error(errMsg, $scope); + } + + /** + * delete a user mapping item + * @param {String} topcoderUsername tc handle + */ + function _handleDeleteUserMapping(topcoderUsername) { + UserMappingsService.delete(topcoderUsername).then(function () { + Alert.info('Successfully deleted User Mapping.', $scope); + $rootScope.dialog = null; + $scope.getUserMappings(); + }).catch(function (er) { + _handleError(er, 'Error deleting user mapping.'); + }); + } + + $scope.deleteUserMapping = function (userMapping) { + $rootScope.dialog = { + topcoderUsername: userMapping.topcoderUsername, + proceed: false, + }; + + // $log.warn(watcher, $scope); + $scope.$on('dialog.finished', function (event, args) { + if (args.proceed) { + _handleDeleteUserMapping($rootScope.dialog.topcoderUsername); + } else { + $rootScope.dialog = {}; + } + }); + Dialog.show('Are you sure you want to delete this User Mapping?', $scope); + }; + + /** + * handles the sort click + * @param criteria the criteria + */ + $scope.sort = function (criteria) { + if (criteria === $scope.tableConfig.sortBy) { + if ($scope.tableConfig.sortDir === 'asc') { + $scope.tableConfig.sortDir = 'desc'; + } else { + $scope.tableConfig.sortDir = 'asc'; + } + } else { + $scope.tableConfig.sortDir = 'asc'; + } + $scope.tableConfig.sortBy = criteria; + $scope.tableConfig.pageNumber = 1; + $scope.getUserMappings(); + }; + + /** + * handles the change page click + * @param {Number} pageNumber the page number + */ + $scope.changePage = function (pageNumber) { + if (pageNumber === 0 || pageNumber > $scope.tableConfig.pages || + (pageNumber === $scope.tableConfig.pages && + $scope.tableConfig.pageNumber === pageNumber)) { + return false; + } + $scope.tableConfig.pageNumber = pageNumber; + $scope.getUserMappings(); + }; + + $scope.onSearchIconClicked = function () { + $scope.tableConfig.pageNumber = 1; + $scope.getUserMappings(); + }; + + $scope.onSearchReset = function () { + var config = $scope.tableConfig; + config.query = ''; + $scope.tableConfig.pageNumber = 1; + $scope.getUserMappings(); + }; + + /** + * get the number array that shows the pagination bar + */ + $scope.getPageArray = function () { + var res = []; + + var pageNo = $scope.tableConfig.pageNumber; + var i = pageNo - 5; + for (i; i <= pageNo; i++) { + if (i > 0) { + res.push(i); + } + } + var j = pageNo + 1; + for (j; j <= $scope.tableConfig.pages && j <= pageNo + 5; j++) { + res.push(j); + } + return res; + }; + + function onInit() { + const domain = window.location.origin; + if (domain.includes('.topcoder-dev.com')) { + $scope.topcoderUrl = 'https://topcoder-dev.com'; + } else { + $scope.topcoderUrl = 'https://topcoder.com'; + } + } + + onInit(); + } + ]); diff --git a/src/front/src/app/user-mappings/user-mappings.html b/src/front/src/app/user-mappings/user-mappings.html new file mode 100644 index 0000000..167c239 --- /dev/null +++ b/src/front/src/app/user-mappings/user-mappings.html @@ -0,0 +1,120 @@ +
+
+
+
+
+

User Mappings

+
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+

You don't have any User Mappings, Please + +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Topcoder Username + + + Github Username + + Github UserID + Gitlab Username + + Gitlab UserIDActions
{{userMapping.topcoderUsername}}{{userMapping.githubUsername}}{{userMapping.githubUserId}}{{userMapping.gitlabUsername}}{{userMapping.gitlabUserId}} + + +
+ +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/front/src/app/user-mappings/user-mappings.service.js b/src/front/src/app/user-mappings/user-mappings.service.js new file mode 100644 index 0000000..51fe68b --- /dev/null +++ b/src/front/src/app/user-mappings/user-mappings.service.js @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + * + * This is a service to access the backend api. + */ +'use strict'; + +angular.module('topcoderX') + .factory('UserMappingsService', ['$http', 'Helper', function ($http, Helper) { + var baseUrl = Helper.baseUrl; + var service = {}; + + /** + * searches user mapping items + * @param {String} query the query string + * @param {String} sortBy the sort by + * @param {String} sortDir the sort direction + * @param {Number} pageNo the page number + * @param {Number} pageSize the page size + */ + service.search = function (query, sortBy, sortDir, pageNo, pageSize, lastKey) { + if (query) return service.searchWithQuery(query, sortBy, sortDir, pageNo, pageSize, lastKey); + else return $http.get(baseUrl + '/api/v1/users/mappings?sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize + + (lastKey && lastKey.githubLastKey ? '&githubLastKey=' + lastKey.githubLastKey : '' ) + + (lastKey && lastKey.gitlabLastKey ? '&gitlabLastKey=' + lastKey.gitlabLastKey : '' )) + .then(function (response) { + return response; + }); + }; + + /** + * searches user mapping items with query + * @param {String} query the query string + * @param {String} sortBy the sort by + * @param {String} sortDir the sort direction + * @param {Number} pageNo the page number + * @param {Number} pageSize the page size + */ + service.searchWithQuery = function (query, sortBy, sortDir, pageNo, pageSize, lastKey) { + return $http.get(baseUrl + '/api/v1/users/mappings?query=' + query + '&sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize + + (lastKey && lastKey.githubLastKey ? '&githubLastKey=' + lastKey.githubLastKey : '' ) + + (lastKey && lastKey.gitlabLastKey ? '&gitlabLastKey=' + lastKey.gitlabLastKey : '' )) + .then(function (response) { + return response; + }); + }; + + /** + * create a new user mapping item + * + */ + service.create = function (bodyParam) { + return $http.post(baseUrl + '/api/v1/users/mappings/', { userMapping: bodyParam }).then(function (response) { + return response; + }); + }; + + /** + * update pre-existing payment item + * + */ + service.update = function (bodyParam) { + return $http.put(baseUrl + '/api/v1/users/mappings/', { userMapping: bodyParam }).then(function (response) { + return response; + }); + }; + + /** + * remove user mapping item + * + */ + service.delete = function (topcoderUsername) { + return $http.delete(baseUrl + '/api/v1/users/mappings/' + topcoderUsername).then(function (response) { + return response.data; + }); + }; + + return service; + }]); diff --git a/src/front/src/components/common/navigation.controller.js b/src/front/src/components/common/navigation.controller.js index 9c7ed28..77595c9 100644 --- a/src/front/src/components/common/navigation.controller.js +++ b/src/front/src/components/common/navigation.controller.js @@ -12,14 +12,25 @@ angular.module('topcoderX') // eslint-disable-line angular/no-services const decodedToken = jwtHelper.decodeToken(token); $scope.user = {}; $scope.user['copilot'] = false; + $scope.user['admin'] = false; Object.keys(decodedToken).findIndex(function (key) { if (key.includes('roles')) { - if (key.indexOf('copilot') > -1) { + if (key.indexOf('copilot') > -1 || decodedToken[key].includes('copilot')) { $scope.user['copilot'] = true; $log.info('User is a copilot'); } else { $log.info('user is not a copilot'); } + + var administratorRoles = $rootScope.appConfig.administratorRoles.map(function (x) { + return x.toLowerCase(); + }); + administratorRoles.forEach(function (administratorRole) { + if (decodedToken[key].includes(administratorRole)) { + $scope.user['admin'] = true; + $log.info('User is an admin'); + } + }); return true; } return false; diff --git a/src/front/src/components/common/navigation.html b/src/front/src/components/common/navigation.html index 3797e88..5f613b4 100644 --- a/src/front/src/components/common/navigation.html +++ b/src/front/src/components/common/navigation.html @@ -31,6 +31,18 @@ Copilot Payments +
  • + + + Github PATs + +
  • +
  • + + + User Mappings + +
  • diff --git a/src/models/Issue.js b/src/models/Issue.js index 038c4d1..509cb76 100644 --- a/src/models/Issue.js +++ b/src/models/Issue.js @@ -35,6 +35,9 @@ const schema = new Schema({ name: 'RepositoryIdIndex', }, }, + repoUrl: { + type: String + }, labels: { type: Array, required: false, diff --git a/src/models/Project.js b/src/models/Project.js index 621c194..ea10c7a 100644 --- a/src/models/Project.js +++ b/src/models/Project.js @@ -24,24 +24,12 @@ const schema = new Schema({ type: Number, required: true }, - repoUrl: { - type: String, - required: true, - index: { - global: true, - rangeKey: 'archived', - project: true, - name: 'RepoUrlIndex' - } - }, - repoId: {type: String, required: false}, rocketChatWebhook: {type: String, required: false}, rocketChatChannelName: {type: String, required: false}, archived: {type: String, required: true}, owner: {type: String, required: true}, secretWebhookKey: {type: String, required: true}, copilot: {type: String, required: false}, - registeredWebhookId: {type: String, required: false}, updatedAt: { type: Date, default: Date.now, diff --git a/src/models/Repository.js b/src/models/Repository.js new file mode 100644 index 0000000..60a958b --- /dev/null +++ b/src/models/Repository.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + */ +'use strict'; + +/** + * Schema for project and repository mapping. + * @author TCSCODER + * @version 1.0 + */ +const dynamoose = require('dynamoose'); + +const Schema = dynamoose.Schema; + +const schema = new Schema({ + id: { + type: String, + hashKey: true, + required: true + }, + projectId: { + type: String, + required: true, + index: { + global: true, + project: true, + name: 'ProjectIdIndex' + }, + }, + url: { + type: String, + required: true, + index: { + global: true, + project: true, + rangKey: 'archived', + name: 'URLIndex' + } + }, + archived: {type: String, required: true}, + repoId: {type: String, required: false}, + registeredWebhookId: {type: String, required: false} +}); + +module.exports = schema; diff --git a/src/routes.js b/src/routes.js index 8832a8b..53e6884 100644 --- a/src/routes.js +++ b/src/routes.js @@ -226,10 +226,6 @@ module.exports = { controller: 'IssueController', method: 'search', }, - post: { - controller: 'IssueController', - method: 'create', - }, }, '/issues/recreate': { post: { @@ -245,4 +241,47 @@ module.exports = { allowAnonymous: true, }, }, + '/github/pat': { + get: { + controller: 'GithubPATsController', + method: 'search', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + post: { + controller: 'GithubPATsController', + method: 'create', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + }, + '/github/pat/:id': { + delete: { + controller: 'GithubPATsController', + method: 'remove', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + }, + '/users/mappings': { + get: { + controller: 'UserController', + method: 'search', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + post: { + controller: 'UserController', + method: 'create', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + put: { + controller: 'UserController', + method: 'update', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + }, + '/users/mappings/:username': { + delete: { + controller: 'UserController', + method: 'remove', + allowedRoles: config.ADMINISTRATOR_ROLES, + }, + }, }; diff --git a/src/services/GithubPATsService.js b/src/services/GithubPATsService.js new file mode 100644 index 0000000..f08a0b2 --- /dev/null +++ b/src/services/GithubPATsService.js @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + */ + +/** + * This service will provide project operations. + * + * @author TCSCODER + * @version 1.0 + */ +const Joi = require('joi'); +const _ = require('lodash'); +const models = require('../models'); +const helper = require('../common/helper'); +const dbHelper = require('../common/db-helper'); + +const Organisation = models.Organisation; + +const searchSchema = { + criteria: Joi.object().keys({ + sortBy: Joi.string().valid('name', 'owner').required(), + sortDir: Joi.string().valid('asc', 'desc').default('asc'), + page: Joi.number().integer().min(1).required(), + perPage: Joi.number().integer().min(1).required(), + }).required(), +}; + +const createPATSchema = { + pat: { + name: Joi.string().required(), + owner: Joi.string().required(), + personalAccessToken: Joi.string().required(), + }, +}; + +const removePATSchema = { + id: Joi.string().required(), +}; + +/** + * searches pats + * @param {Object} criteria the search criteria + * @returns {Array} pats + */ +async function search(criteria) { + const pats = await dbHelper.scan(Organisation, {}); + const filteredPats = _.map(pats, (pat) => {return {id: pat.id, name: pat.name, owner: pat.owner}}); + const offset = (criteria.page - 1) * criteria.perPage; + const result = { + pages: Math.ceil(filteredPats.length / criteria.perPage) || 1, + docs: _(filteredPats).orderBy(criteria.sortBy, criteria.sortDir) + .slice(offset).take(criteria.perPage) + .value(), + }; + return result; +} + +search.schema = searchSchema; + +/** + * creates pat + * @param {Object} pat details + * @returns {Object} created pat + */ +async function create(pat) { + const existPAT = await dbHelper.queryOneOrganisation(Organisation, pat.name); + if (existPAT) { + return { error: true, exist: true }; + } + + pat.id = helper.generateIdentifier(); + + let dbPat = await dbHelper.create(Organisation, pat); + return {id: dbPat.id, name: dbPat.name, owner: dbPat.owner}; +} + +create.schema = createPATSchema; + +/** + * delete payment item + * @param {object} id payment id + * @returns {Object} the success status + */ +async function remove(id) { + await dbHelper.removeById(Organisation, id); + return {success: true}; +} + +remove.schema = removePATSchema; + + +module.exports = { + search, + create, + remove, +}; + +helper.buildService(module.exports); diff --git a/src/services/GithubService.js b/src/services/GithubService.js index fb8d2f5..df559c8 100644 --- a/src/services/GithubService.js +++ b/src/services/GithubService.js @@ -331,7 +331,7 @@ async function getUserIdByUsername(username) { } return user.data.id; } catch (err) { - throw helper.convertGitHubError(err, 'Failed to get detail about user from github'); + throw new Error(`The user with username ${username} is not found on github`); } } diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js index 06ede47..4144407 100644 --- a/src/services/GitlabService.js +++ b/src/services/GitlabService.js @@ -251,7 +251,7 @@ async function getUserIdByUsername(username) { } return users[0].id; } catch (err) { - throw helper.convertGitLabError(err, 'Failed to get detail about user from gitlab.'); + throw helper.convertGitLabError(err, 'Failed to get detail about user from gitlab'); } } diff --git a/src/services/IssueService.js b/src/services/IssueService.js index 6732f91..e2882b6 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -64,6 +64,10 @@ async function search(criteria, currentUserTopcoderHandle) { for (const issue of docs) { // eslint-disable-line guard-for-in,no-restricted-syntax issue.projectId = await dbHelper.getById(models.Project, issue.projectId); issue.assignedAt = moment(issue.assignedAt).format('YYYY-MM-DD HH:mm:ss'); + if (!issue.repoUrl) { + const repoUrls = await dbHelper.populateRepoUrls(issue.projectId.id); + issue.repoUrl = repoUrls && repoUrls.length > 0 ? repoUrls[0] : undefined; + } } const offset = (criteria.page - 1) * criteria.perPage; @@ -116,91 +120,6 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { return dbProject; } -/** - * create issue - * @param {Object} issue the issue detail - * @param {String} currentUser the topcoder current user - * @returns {Object} created issue - */ -async function create(issue, currentUser) { - const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrl.split('/'); - const index = 1; - const repoName = results[results.length - index]; - const excludePart = 3; - const repoOwner = _(results).slice(excludePart, results.length - 1).join('/'); - const title = `[$${issue.prize}] ${issue.title}`; - if (provider === 'github') { - try { - const github = new GitHub({token: userRole.accessToken}); - const githubIssueWrapper = github.getIssues(repoOwner, repoName); - const newIssue = { - title, - body: issue.comment, - labels: [config.OPEN_FOR_PICKUP_ISSUE_LABEL], - }; - const createdIssueResp = await githubIssueWrapper.createIssue(newIssue); - const createdIssueData = createdIssueResp.data; - return { - success: true, - url: createdIssueData.html_url, - number: createdIssueData.number, - }; - } catch (err) { - // if error is already exists discard - if (_.chain(err).get('body.errors').countBy({ - code: 'already_exists', - }).get('true') - .isUndefined() - .value()) { - throw helper.convertGitHubError(err, 'Failed to create issue.'); - } - } - } else { - try { - const client = new Gitlab({ - url: config.GITLAB_API_BASE_URL, - oauthToken: userRole.accessToken, - }); - const response = await client.Issues.create(`${repoOwner}/${repoName}`, { - title, - description: issue.comment, - labels: config.OPEN_FOR_PICKUP_ISSUE_LABEL, - }); - return { - success: true, - url: response.web_url, - number: response.iid - }; - } catch (err) { - if (_.get(err, 'error.message') !== 'Label already exists') { - throw helper.convertGitLabError(err, 'Failed to create labels.'); - } - } - } - return { - success: false, - }; -} - -const currentUserSchema = Joi.object().keys({ - handle: Joi.string().required(), - roles: Joi.array().required(), -}); - -create.schema = { - issue: { - projectId: Joi.string().required(), - prize: Joi.number().required(), - title: Joi.string().required(), - comment: Joi.string().required(), - repoUrl: Joi.string().required(), - }, - currentUser: currentUserSchema, -}; - /** * recreate issue * @param {Object} issue the issue detail @@ -209,9 +128,9 @@ create.schema = { */ async function recreate(issue, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); + const provider = await helper.getProviderType(issue.url); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrl.split('/'); + const results = issue.url.split('/'); const index = 1; const repoName = results[results.length - index]; const excludePart = 3; @@ -304,6 +223,11 @@ async function recreate(issue, currentUser) { }; } +const currentUserSchema = Joi.object().keys({ + handle: Joi.string().required(), + roles: Joi.array().required(), +}); + recreate.schema = { issue: { projectId: Joi.string().required(), @@ -316,7 +240,6 @@ recreate.schema = { module.exports = { search, - create, recreate, }; diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index 7383e23..a23ac94 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -37,6 +37,7 @@ const projectSchema = { title: Joi.string().required(), tcDirectId: Joi.number().required(), repoUrl: Joi.string().required(), + repoUrls: Joi.array().required(), rocketChatWebhook: Joi.string().allow(null), rocketChatChannelName: Joi.string().allow(null), archived: Joi.boolean().required(), @@ -69,20 +70,21 @@ const createProjectSchema = { /** * ensures the requested project detail is valid * @param {Object} project the project detail + * @param {String} repoUrl the repo url * @private */ -async function _validateProjectData(project) { +async function _validateProjectData(project, repoUrl) { let existsInDatabase; if (project.id) { - existsInDatabase = await dbHelper.queryOneActiveProjectWithFilter(models.Project, project.repoUrl, project.id) + existsInDatabase = await dbHelper.queryOneActiveProjectWithFilter(models.Project, repoUrl, project.id) } else { - existsInDatabase = await dbHelper.queryOneActiveProject(models.Project, project.repoUrl) + existsInDatabase = await dbHelper.queryOneActiveProject(models.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}`) } - const provider = await helper.getProviderType(project.repoUrl); + const provider = await helper.getProviderType(repoUrl); const userRole = project.copilot ? project.copilot : project.owner; const setting = await userService.getUserSetting(userRole); if (!setting[provider]) { @@ -122,7 +124,10 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { async function create(project, currentUser) { const currentUserTopcoderHandle = currentUser.handle; project.owner = currentUserTopcoderHandle; - await _validateProjectData(project); + const repoUrls = _.map(project.repoUrl.split(','), repoUrl => repoUrl.trim()); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await _validateProjectData(project, repoUrl); + } /** * Uncomment below code to enable the function of raising event when 'project was created' * @@ -139,13 +144,21 @@ async function create(project, currentUser) { const createdProject = await dbHelper.create(models.Project, project); - try { - await createLabel({projectId: project.id}, currentUser); - await createHook({projectId: project.id}, currentUser); - await addWikiRules({projectId: project.id}, currentUser); - } - catch (err) { - throw new Error('Project created. Adding the webhook, issue labels, and wiki rules failed.'); + 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); + } + catch (err) { + throw new Error(`Project created. Adding the webhook, issue labels, and wiki rules failed. Repo ${repoUrl}`); + } } return createdProject; @@ -161,7 +174,10 @@ create.schema = createProjectSchema; */ async function update(project, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(project.id, currentUser); - await _validateProjectData(project); + const repoUrls = _.map(project.repoUrl.split(','), repoUrl => repoUrl.trim()); + for (const repoUrl of repoUrls) { // eslint-disable-line no-restricted-syntax + await _validateProjectData(project, repoUrl); + } if (dbProject.archived === 'false' && project.archived === true) { // project archived detected. const result = { @@ -185,8 +201,19 @@ async function update(project, currentUser) { dbProject[item[0]] = item[1]; return item; }); + const oldRepositories = await dbHelper.queryRepositoriesByProjectId(dbProject.id); + for (const repo of oldRepositories) { // eslint-disable-line + 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 + }) + } dbProject.updatedAt = new Date(); - return await dbHelper.update(models.Project, dbProject.id, dbProject); } @@ -215,6 +242,9 @@ async function getAll(query, currentUser) { } return project; }); + for (const project of projects) { // eslint-disable-line + project.repoUrls = await dbHelper.populateRepoUrls(project.id); + } return _.orderBy(projects, ['updatedAt', 'title'], ['desc', 'asc']); } @@ -237,6 +267,9 @@ async function getAll(query, currentUser) { } return project; }); + for (const project of projects) { // eslint-disable-line + project.repoUrls = await dbHelper.populateRepoUrls(project.id); + } return _.orderBy(projects, ['updatedAt', 'title'], ['desc', 'asc']); } @@ -252,13 +285,14 @@ getAll.schema = Joi.object().keys({ * creates label * @param {Object} body the request body * @param {String} currentUser the topcoder current user + * @param {String} repoUrl the repo url of the project * @returns {Object} result */ -async function createLabel(body, currentUser) { +async function createLabel(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); + const provider = await helper.getProviderType(repoUrl); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrl.split('/'); + const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; const excludePart = 3; @@ -318,33 +352,36 @@ createLabel.schema = Joi.object().keys({ projectId: Joi.string().required(), }), currentUser: currentUserSchema, + repoUrl: Joi.string().required() }); /** * creates hook * @param {Object} body the request body * @param {String} currentUser the topcoder current user + * @param {String} repoUrl the repo url of the project * @returns {Object} result */ -async function createHook(body, currentUser) { +async function createHook(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); + const dbRepo = await dbHelper.queryRepositoryByProjectIdFilterUrl(dbProject.id, repoUrl); + const provider = await helper.getProviderType(repoUrl); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); - const results = dbProject.repoUrl.split('/'); + const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; const excludePart = 3; const repoOwner = _(results).slice(excludePart, results.length - 1).join('/'); - const updateExisting = dbProject.registeredWebhookId !== undefined; + const updateExisting = dbRepo.registeredWebhookId !== undefined; if (provider === 'github') { try { const github = new GitHub({token: userRole.accessToken}); const repoWrapper = github.getRepo(repoOwner, repoName); await new Promise((resolve, reject) => { repoWrapper.listHooks(async (err, hooks) => { - if (!err && dbProject.registeredWebhookId && - _.find(hooks, {id: parseInt(dbProject.registeredWebhookId, 10)})) { - await repoWrapper.deleteHook(dbProject.registeredWebhookId); + if (!err && dbRepo.registeredWebhookId && + _.find(hooks, {id: parseInt(dbRepo.registeredWebhookId, 10)})) { + await repoWrapper.deleteHook(dbRepo.registeredWebhookId); } repoWrapper.createHook({ name: 'web', @@ -363,13 +400,13 @@ async function createHook(body, currentUser) { content_type: 'json', secret: dbProject.secretWebhookKey, }, - }, (error, hook) => { + }, async (error, hook) => { if (error) { return reject(error); } if (hook && hook.id) { - dbProject.registeredWebhookId = hook.id.toString(); - update(dbProject, currentUser); + dbRepo.registeredWebhookId = hook.id.toString(); + await dbHelper.update(models.Repository, dbRepo.id, dbRepo); } return resolve(); }); @@ -396,9 +433,9 @@ async function createHook(body, currentUser) { oauthToken: userRole.accessToken, }); const hooks = await client.ProjectHooks.all(`${repoOwner}/${repoName}`); - if (hooks && dbProject.registeredWebhookId && - _.find(hooks, {id: parseInt(dbProject.registeredWebhookId, 10)})) { - await client.ProjectHooks.remove(`${repoOwner}/${repoName}`, dbProject.registeredWebhookId); + if (hooks && dbRepo.registeredWebhookId && + _.find(hooks, {id: parseInt(dbRepo.registeredWebhookId, 10)})) { + await client.ProjectHooks.remove(`${repoOwner}/${repoName}`, dbRepo.registeredWebhookId); } const hook = await client.ProjectHooks.add(`${repoOwner}/${repoName}`, `${config.HOOK_BASE_URL}/webhooks/gitlab`, { @@ -415,8 +452,8 @@ async function createHook(body, currentUser) { } ); if (hook && hook.id) { - dbProject.registeredWebhookId = hook.id.toString(); - await update(dbProject, currentUser); + dbRepo.registeredWebhookId = hook.id.toString(); + await dbHelper.update(models.Repository, dbRepo.id, dbRepo); } } catch (err) { const errMsg = 'Failed to create webhook'; @@ -443,13 +480,14 @@ createHook.schema = createLabel.schema; * adds the wiki rules the project's repository * @param {Object} body the request body * @param {String} currentUser the topcoder current user + * @param {String} repoUrl the repo url of the project * @returns {Object} result */ -async function addWikiRules(body, currentUser) { +async function addWikiRules(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); + const provider = await helper.getProviderType(repoUrl); const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, dbProject.copilot !== undefined); - const results = dbProject.repoUrl.split('/'); + const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; const excludePart = 3; @@ -511,14 +549,18 @@ async function transferOwnerShip(body, currentUser) { throw new errors.ForbiddenError('You can\'t transfer the ownership of this project'); } const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); - const provider = await helper.getProviderType(dbProject.repoUrl); + + const repoUrls = await dbHelper.populateRepoUrls(dbProject.id); const setting = await userService.getUserSetting(body.owner); - if (!setting.gitlab && !setting.github) { - throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access. - Please have them sign in and set up their Gitlab and Github accounts with Topcoder-X before transferring ownership.`); - } else if (!setting[provider]) { - throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access setup for ${provider}. - Please have them sign in and set up their ${provider} accounts with Topcoder-X before transferring ownership.`); + for (const repoUrl of repoUrls) { // eslint-disable-line + const provider = await helper.getProviderType(repoUrl); + if (!setting.gitlab && !setting.github) { + throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access. + Please have them sign in and set up their Gitlab and Github accounts with Topcoder-X before transferring ownership.`); + } else if (!setting[provider]) { + throw new errors.ValidationError(`User ${body.owner} doesn't currently have Topcoder-X access setup for ${provider}. + Please have them sign in and set up their ${provider} accounts with Topcoder-X before transferring ownership.`); + } } return await dbHelper.update(models.Project, dbProject.id, { diff --git a/src/services/UserService.js b/src/services/UserService.js index d04a0d0..f706844 100644 --- a/src/services/UserService.js +++ b/src/services/UserService.js @@ -17,6 +17,8 @@ const constants = require('../common/constants'); const User = require('../models').User; const GithubUserMapping = require('../models').GithubUserMapping; const GitlabUserMapping = require('../models').GitlabUserMapping; +const GithubService = require('./GithubService'); +const GitlabService = require('./GitlabService'); /** * gets user setting @@ -145,11 +147,253 @@ getUserToken.schema = Joi.object().keys({ tokenType: Joi.string().required(), }); + + +const searchSchema = { + criteria: Joi.object().keys({ + sortBy: Joi.string().valid('topcoderUsername', 'githubUsername', 'gitlabUsername').required(), + sortDir: Joi.string().valid('asc', 'desc').default('asc'), + page: Joi.number().integer().min(1).required(), + perPage: Joi.number().integer().min(1).required(), + query: Joi.string(), + gitlabLastKey: Joi.string(), + githubLastKey: Joi.string() + }).required(), +}; + +const createUserMappingSchema = { + userMapping: { + topcoderUsername: Joi.string().required(), + githubUsername: Joi.string(), + githubUserId: Joi.number(), + gitlabUsername: Joi.string(), + gitlabUserId: Joi.number(), + }, +}; + +const removeUserMappingSchema = { + topcoderUsername: Joi.string().required(), +}; + +/** + * searches user mappings + * @param {Object} criteria the search criteria + * @returns {Array} user mappings + */ +async function search(criteria) { + let githubUserMappings; + let gitlabUserMappings; + + if (criteria.query) { + githubUserMappings = await dbHelper.scanAllWithSearch( + GithubUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.githubLastKey ? JSON.parse(criteria.githubLastKey) : undefined, // eslint-disable-line + 'topcoderUsername', + criteria.query.toLowerCase()); + gitlabUserMappings = await dbHelper.scanAllWithSearch( + GitlabUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.gitlabLastKey ? JSON.parse(criteria.gitlabLastKey) : undefined, // eslint-disable-line + 'topcoderUsername', + criteria.query.toLowerCase()); + } + else { + githubUserMappings = await dbHelper.scanAll( + GithubUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.githubLastKey ? JSON.parse(criteria.githubLastKey) : undefined); // eslint-disable-line + gitlabUserMappings = await dbHelper.scanAll( + GitlabUserMapping, + criteria.perPage / 2, // eslint-disable-line + criteria.gitlabLastKey ? JSON.parse(criteria.gitlabLastKey) : undefined); // eslint-disable-line + } + + const userMappings = _.concat(githubUserMappings, gitlabUserMappings); + const orderedUserMappings = _.orderBy(userMappings, criteria.sortBy, criteria.sortDir); + const tcUsernames = _.map(orderedUserMappings, 'topcoderUsername'); + const uniqueTcUsernames = _.uniq(tcUsernames); + const docs = await Promise.all(_.map(uniqueTcUsernames, async (tcUsername) => { + const mapping = { + topcoderUsername: tcUsername + }; + const githubMapping = _.find(githubUserMappings, (object) => object.topcoderUsername === tcUsername); // eslint-disable-line lodash/matches-prop-shorthand + const gitlabMapping = _.find(gitlabUserMappings, (object) => object.topcoderUsername === tcUsername); // eslint-disable-line lodash/matches-prop-shorthand + if (githubMapping) { + mapping.githubUsername = githubMapping.githubUsername; + mapping.githubUserId = githubMapping.githubUserId; + } + else { + const dbGithubMapping = await dbHelper.queryOneUserMappingByTCUsername(GithubUserMapping, tcUsername); + if (dbGithubMapping) { + mapping.githubUsername = dbGithubMapping.githubUsername; + mapping.githubUserId = dbGithubMapping.githubUserId; + } + } + if (gitlabMapping) { + mapping.gitlabUsername = gitlabMapping.gitlabUsername; + mapping.gitlabUserId = gitlabMapping.gitlabUserId; + } + else { + const dbGitlabMapping = await dbHelper.queryOneUserMappingByTCUsername(GitlabUserMapping, tcUsername); + if (dbGitlabMapping) { + mapping.gitlabUsername = dbGitlabMapping.gitlabUsername; + mapping.gitlabUserId = dbGitlabMapping.gitlabUserId; + } + } + return mapping; + })); + + const result = { + lastKey : { + githubLastKey: githubUserMappings.lastKey ? JSON.stringify(githubUserMappings.lastKey) : undefined, // eslint-disable-line + gitlabLastKey: gitlabUserMappings.lastKey ? JSON.stringify(gitlabUserMappings.lastKey) : undefined // eslint-disable-line + }, + docs, + }; + return result; +} + +search.schema = searchSchema; + +/** + * creates userMapping + * @param {Object} userMapping details + * @returns {Object} created userMapping + */ +async function create(userMapping) { + if (userMapping.githubUsername) { + const existGithubMapping = await dbHelper.queryOneUserMappingByTCUsername( + GithubUserMapping, userMapping.topcoderUsername); + if (existGithubMapping) { + return { error: true, exist: true, provider: 'Github' }; + } + else { + const githubUserId = await GithubService.getUserIdByUsername(userMapping.githubUsername); + const mappingToSave = { + id: helper.generateIdentifier(), + topcoderUsername: userMapping.topcoderUsername, + githubUsername: userMapping.githubUsername, + githubUserId + }; + await dbHelper.create(GithubUserMapping, mappingToSave); + } + } + if (userMapping.gitlabUsername) { + const existGitlabMapping = await dbHelper.queryOneUserMappingByTCUsername( + GitlabUserMapping, userMapping.topcoderUsername); + if (existGitlabMapping) { + return { error: true, exist: true, provider: 'Gitlab' }; + } + else { + const gitlabUserId = await GitlabService.getUserIdByUsername(userMapping.gitlabUsername); + const mappingToSave = { + id: helper.generateIdentifier(), + topcoderUsername: userMapping.topcoderUsername, + gitlabUsername: userMapping.gitlabUsername, + gitlabUserId + }; + await dbHelper.create(GitlabUserMapping, mappingToSave); + } + } + + return {success: true}; +} + +create.schema = createUserMappingSchema; + + + +/** + * updates userMapping + * @param {Object} userMapping details + * @returns {Object} updated userMapping + */ +async function update(userMapping) { + const existGithubMapping = await dbHelper.queryOneUserMappingByTCUsername( + GithubUserMapping, userMapping.topcoderUsername); + const existGitlabMapping = await dbHelper.queryOneUserMappingByTCUsername( + GitlabUserMapping, userMapping.topcoderUsername); + if (userMapping.githubUsername) { + const githubUserId = await GithubService.getUserIdByUsername(userMapping.githubUsername); + const mappingToSave = { + topcoderUsername: userMapping.topcoderUsername, + githubUsername: userMapping.githubUsername, + githubUserId + }; + if (existGithubMapping) { + mappingToSave.id = existGithubMapping.id; + await dbHelper.update(GithubUserMapping, existGithubMapping.id, mappingToSave); + } + else { + mappingToSave.id = helper.generateIdentifier(); + await dbHelper.create(GithubUserMapping, mappingToSave); + } + } + else { + if (existGithubMapping) { + await dbHelper.removeById(GithubUserMapping, existGithubMapping.id); + } + } + if (userMapping.gitlabUsername) { + const gitlabUserId = await GitlabService.getUserIdByUsername(userMapping.gitlabUsername); + const mappingToSave = { + topcoderUsername: userMapping.topcoderUsername, + gitlabUsername: userMapping.gitlabUsername, + gitlabUserId + }; + if (existGitlabMapping) { + mappingToSave.id = existGitlabMapping.id; + await dbHelper.update(GitlabUserMapping, existGitlabMapping.id, mappingToSave); + } + else { + mappingToSave.id = helper.generateIdentifier(); + await dbHelper.create(GitlabUserMapping, mappingToSave); + } + } + else { + if (existGitlabMapping) { + await dbHelper.removeById(GitlabUserMapping, existGitlabMapping.id); + } + } + return {success: true}; +} + +update.schema = createUserMappingSchema; + + + +/** + * delete user mapping item + * @param {string} topcoderUsername tc handle + * @returns {Object} the success status + */ +async function remove(topcoderUsername) { + const dbGithubMapping = await dbHelper.queryOneUserMappingByTCUsername( + GithubUserMapping, topcoderUsername); + const dbGitlabMapping = await dbHelper.queryOneUserMappingByTCUsername( + GitlabUserMapping, topcoderUsername); + + if (dbGithubMapping) { + await dbHelper.removeById(GithubUserMapping, dbGithubMapping.id); + } + if (dbGitlabMapping) { + await dbHelper.removeById(GitlabUserMapping, dbGitlabMapping.id); + } + return {success: true}; +} + +remove.schema = removeUserMappingSchema; + module.exports = { getUserSetting, revokeUserSetting, getUserToken, getAccessTokenByHandle, + search, + create, + remove, + update, }; helper.buildService(module.exports);