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); pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy