diff --git a/src/common/helper.js b/src/common/helper.js index 5d7f21d..017a00c 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -166,6 +166,26 @@ function convertGitLabError(err, message) { return apiError; } +/** + * Convert azure api error. + * @param {Error} err the azure api error + * @param {String} message the error message + * @returns {Error} converted error + */ +function convertAzureError(err, message) { + let resMsg = `${message}. ${err.message}.\n`; + const detail = _.get(err, 'response.body.message'); + if (detail) { + resMsg += ` Detail: ${detail}`; + } + const apiError = new errors.ApiError( + err.status || _.get(err, 'response.status', constants.SERVICE_ERROR_STATUS), + _.get(err, 'response.body.message', constants.SERVICE_ERROR), + resMsg + ); + return apiError; +} + /** * Ensure entity exists for given criteria. Return error if no result. * @param {Object} Model the mongoose model to query @@ -270,6 +290,7 @@ module.exports = { buildController, convertGitHubError, convertGitLabError, + convertAzureError, ensureExists, generateIdentifier, getProviderType, diff --git a/src/config.js b/src/config.js index fe3e983..a6377d4 100644 --- a/src/config.js +++ b/src/config.js @@ -93,7 +93,8 @@ const frontendConfigs = { "OWNER_LOGIN_AZURE_URL":"/api/v1/azure/owneruser/login", "TOPCODER_URL": "https://topcoder-dev.com", "GITHUB_TEAM_URL": "https://github.com/orgs/", - "GITLAB_GROUP_URL": "https://gitlab.com/groups/" + "GITLAB_GROUP_URL": "https://gitlab.com/groups/", + "AZURE_TEAM_URL": "https://dev.azure.com/" }, "heroku":{ @@ -111,7 +112,8 @@ const frontendConfigs = { "OWNER_LOGIN_AZURE_URL":"/api/v1/azure/owneruser/login", "TOPCODER_URL": "https://topcoder-dev.com", "GITHUB_TEAM_URL": "https://github.com/orgs/", - "GITLAB_GROUP_URL": "https://gitlab.com/groups/" + "GITLAB_GROUP_URL": "https://gitlab.com/groups/", + "AZURE_TEAM_URL": "https://dev.azure.com/" }, "dev":{ "JWT_V3_NAME":"v3jwt", @@ -128,7 +130,8 @@ const frontendConfigs = { "OWNER_LOGIN_AZURE_URL":"/api/v1/azure/owneruser/login", "TOPCODER_URL": "https://topcoder-dev.com", "GITHUB_TEAM_URL": "https://github.com/orgs/", - "GITLAB_GROUP_URL": "https://gitlab.com/groups/" + "GITLAB_GROUP_URL": "https://gitlab.com/groups/", + "AZURE_TEAM_URL": "https://dev.azure.com/" }, "qa":{ "JWT_V3_NAME":"v3jwt", @@ -145,7 +148,8 @@ const frontendConfigs = { "OWNER_LOGIN_AZURE_URL":"/api/v1/azure/owneruser/login", "TOPCODER_URL": "https://topcoder-dev.com", "GITHUB_TEAM_URL": "https://github.com/orgs/", - "GITLAB_GROUP_URL": "https://gitlab.com/groups/" + "GITLAB_GROUP_URL": "https://gitlab.com/groups/", + "AZURE_TEAM_URL": "https://dev.azure.com/" }, "prod":{ "JWT_V3_NAME":"v3jwt", @@ -162,7 +166,8 @@ const frontendConfigs = { "OWNER_LOGIN_AZURE_URL":"/api/v1/azure/owneruser/login", "TOPCODER_URL": "https://topcoder-dev.com", "GITHUB_TEAM_URL": "https://github.com/orgs/", - "GITLAB_GROUP_URL": "https://gitlab.com/groups/" + "GITLAB_GROUP_URL": "https://gitlab.com/groups/", + "AZURE_TEAM_URL": "https://dev.azure.com/" } }; @@ -185,5 +190,6 @@ module.exports.frontendConfigs = { OWNER_LOGIN_AZURE_URL: process.env.OWNER_LOGIN_AZURE_URL || frontendConfigs[activeEnv].OWNER_LOGIN_AZURE_URL, TOPCODER_URL: process.env.TOPCODER_URL || frontendConfigs[activeEnv].TOPCODER_URL, GITHUB_TEAM_URL: process.env.GITHUB_TEAM_URL || frontendConfigs[activeEnv].GITHUB_TEAM_URL, - GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || frontendConfigs[activeEnv].GITLAB_GROUP_URL + GITLAB_GROUP_URL: process.env.GITLAB_GROUP_URL || frontendConfigs[activeEnv].GITLAB_GROUP_URL, + AZURE_TEAM_URL: process.env.AZURE_TEAM_URL || frontendConfigs[activeEnv].AZURE_TEAM_URL }; \ No newline at end of file diff --git a/src/controllers/AzureController.js b/src/controllers/AzureController.js index b82100c..a257e0a 100644 --- a/src/controllers/AzureController.js +++ b/src/controllers/AzureController.js @@ -3,7 +3,7 @@ */ /** - * This controller exposes Gitlab REST endpoints. + * This controller exposes Azure REST endpoints. * * @author TCSCODER * @version 1.0 @@ -17,12 +17,11 @@ const errors = require('../common/errors'); const constants = require('../common/constants'); const config = require('../config'); const AzureService = require('../services/AzureService'); -const GitlabService = require('../services/GitlabService'); const UserService = require('../services/UserService'); const User = require('../models').User; const OwnerUserTeam = require('../models').OwnerUserTeam; -// const UserMapping = require('../models').UserMapping; -const UserGroupMapping = require('../models').UserGroupMapping; +const UserMapping = require('../models').UserMapping; +const UserTeamMapping = require('../models').UserTeamMapping; const request = superagentPromise(superagent, Promise); @@ -40,7 +39,7 @@ async function ownerUserLogin(req, res) { if (!req.session.state) { req.session.state = helper.generateIdentifier(); } - // redirect to GitLab OAuth + // redirect to Azure OAuth const callbackUri = `${config.WEBSITE_SECURE}${constants.AZURE_OWNER_CALLBACK_URL}`; res.redirect(`https://app.vssps.visualstudio.com/oauth2/authorize?client_id=${ config.AZURE_APP_ID @@ -50,7 +49,7 @@ async function ownerUserLogin(req, res) { } /** - * Owner user login callback, redirected by GitLab. + * Owner user login callback, redirected by Azure. * @param {Object} req the request * @param {Object} res the response */ @@ -104,7 +103,7 @@ async function ownerUserLoginCallback(req, res) { async function listOwnerUserTeams(req) { const user = await UserService.getAccessTokenByHandle(req.currentUser.handle, constants.USER_TYPES.AZURE); if (!user || !user.accessToken) { - throw new errors.UnauthorizedError('You have not setup for Gitlab.'); + throw new errors.UnauthorizedError('You have not setup for Azure.'); } return await AzureService.listOwnerUserTeams(user, req.query.page, req.query.perPage); } @@ -136,7 +135,7 @@ async function addUserToTeam(req, res) { // store identifier to session, to be compared in callback req.session.identifier = identifier; - // redirect to GitLab OAuth + // redirect to Azure OAuth const callbackUri = `${config.WEBSITE_SECURE}/api/${config.API_VERSION}/azure/normaluser/callback`; res.redirect(`https://app.vssps.visualstudio.com/oauth2/authorize?client_id=${ config.AZURE_USER_APP_ID @@ -146,7 +145,7 @@ async function addUserToTeam(req, res) { } /** - * Normal user callback, to be added to group. Redirected by GitLab. + * Normal user callback, to be added to group. Redirected by Azure. * @param {Object} req the request * @param {Object} res the response */ @@ -197,29 +196,15 @@ async function addUserToTeamCallback(req, res) { .end() .then((resp) => resp.body); - // PATCH https://vsaex.dev.azure.com/{organization}/_apis/userentitlements/{userId}?api-version=5.1-preview.2 try { - await request - .patch(`https://vsaex.dev.azure.com/telagaid/_apis/userentitlements/${userProfile.id}?api-version=5.1-preview.2`) + await request + .patch(`https://vsaex.dev.azure.com/${team.organizationName}/_apis/UserEntitlements?doNotSendInviteForNewUsers=true&api-version=5.1-preview.3`) .send([{ - from: "", + from: '', op: 0, - path: "", + path: `/${userProfile.id}/projectEntitlements/${team.githubOrgId}/teamRefs`, value: { - projectEntitlements: { - projectRef: { - id: team.githubOrgId - }, - teamRefs: [{ - id:team.teamId - }] - }, - user: { - subjectKind: 'user', - displayName: userProfile.emailAddress, - principalName: userProfile.emailAddress, - id: userProfile.id - } + id:team.teamId } }]) .set('Content-Type', 'application/json-patch+json') @@ -229,37 +214,70 @@ async function addUserToTeamCallback(req, res) { catch(err) { console.log(err); // eslint-disable-line no-console } + + // associate azure username with TC username + const mapping = await dbHelper.scanOne(UserMapping, { + topcoderUsername: {eq: req.session.tcUsername}, + }); + if (mapping) { + await dbHelper.update(UserMapping, mapping.id, { + azureEmail: userProfile.emailAddress, + azureUserId: userProfile.id + }); + } else { + await dbHelper.create(UserMapping, { + id: helper.generateIdentifier(), + topcoderUsername: req.session.tcUsername, + azureEmail: userProfile.emailAddress, + azureUserId: userProfile.id + }); + } + + const azureUserToTeamMapping = await dbHelper.scanOne(UserTeamMapping, { + teamId: {eq: team.teamId}, + azureUserId: {eq: userProfile.id}, + }); + + if (!azureUserToTeamMapping) { + await dbHelper.create(UserTeamMapping, { + id: helper.generateIdentifier(), + teamId: team.teamId, + azureUserId: userProfile.id, + azureProjectId: team.githubOrgId + }); + } + // redirect to success page - res.redirect(`${constants.USER_ADDED_TO_TEAM_SUCCESS_URL}/azure/path`); + res.redirect(`${constants.USER_ADDED_TO_TEAM_SUCCESS_URL}/azure/${team.organizationName}_${team.githubOrgId}`); } /** - * Delete users from a group. + * Delete users from a team. * @param {Object} req the request * @param {Object} res the response */ async function deleteUsersFromTeam(req, res) { - const groupId = req.params.id; - let groupInDB; + const teamId = req.params.id; + let teamInDB; try { - groupInDB = await helper.ensureExists(OwnerUserTeam, {groupId}, 'OwnerUserTeam'); + teamInDB = await helper.ensureExists(OwnerUserTeam, {teamId}, 'OwnerUserTeam'); } catch (err) { if (!(err instanceof errors.NotFoundError)) { throw err; } } - // If groupInDB not exists, then just return - if (groupInDB) { + // If teamInDB not exists, then just return + if (teamInDB) { try { const ownerUser = await helper.ensureExists(User, - {username: groupInDB.ownerUsername, type: constants.USER_TYPES.GITLAB, role: constants.USER_ROLES.OWNER}, 'User'); - await GitlabService.refreshGitlabUserAccessToken(ownerUser); - const userGroupMappings = await dbHelper.scan(UserGroupMapping, {groupId}); + {username: teamInDB.ownerUsername, type: constants.USER_TYPES.AZURE, role: constants.USER_ROLES.OWNER}, 'User'); + await AzureService.refreshAzureUserAccessToken(ownerUser); + const userTeamMappings = await dbHelper.scan(UserTeamMapping, {teamId}); // eslint-disable-next-line no-restricted-syntax - for (const userGroupMapItem of userGroupMappings) { - await GitlabService.deleteUserFromGitlabGroup(ownerUser.accessToken, groupId, userGroupMapItem.gitlabUserId); - await dbHelper.remove(UserGroupMapping, {id: userGroupMapItem.id}); + for (const userTeamMapItem of userTeamMappings) { + await AzureService.deleteUserFromAzureTeam(ownerUser.accessToken, teamInDB, userTeamMapItem.azureUserId); + await dbHelper.remove(UserTeamMapping, {id: userTeamMapItem.id}); } } catch (err) { throw err; diff --git a/src/front/src/app/git-access-control/access-control.service.js b/src/front/src/app/git-access-control/access-control.service.js index 928472a..edbad10 100644 --- a/src/front/src/app/git-access-control/access-control.service.js +++ b/src/front/src/app/git-access-control/access-control.service.js @@ -95,5 +95,15 @@ angular.module('topcoderX') }); }; + /** + * remove all users from a azure team + * + */ + service.removeAllAzureUsers = function (teamId) { + return $http.delete(baseUrl + '/api/v1/azure/teams/' + teamId + '/users').then(function (response) { + return response; + }); + }; + return service; }]); diff --git a/src/front/src/app/members/member.controller.js b/src/front/src/app/members/member.controller.js index b4f1461..7c9634f 100644 --- a/src/front/src/app/members/member.controller.js +++ b/src/front/src/app/members/member.controller.js @@ -11,8 +11,11 @@ angular.module('topcoderX') const org = params[0]; const team = url.replace(org, '').substring(1); $scope.link = $rootScope.appConfig.GITHUB_TEAM_URL + org + '/teams/' + team; - } else { + } else if (provider === 'github') { $scope.link = $rootScope.appConfig.GITLAB_GROUP_URL + url; + } else if (provider === 'azure') { + const params = url.split('_'); + $scope.link = $rootScope.appConfig.AZURE_TEAM_URL + params[0] + '/' + params[1]; } }; _getUrl($scope.provider, $stateParams.url); diff --git a/src/front/src/app/members/member.html b/src/front/src/app/members/member.html index 5d20ef3..bf3f4ff 100644 --- a/src/front/src/app/members/member.html +++ b/src/front/src/app/members/member.html @@ -19,6 +19,10 @@
You were successfully added to the group!
{{link}} +You were successfully added to the Azure DevOps project team!
+ {{link}} +