Skip to content

user access updates #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ The following parameters can be set in config files or in env variables:
- KAFKA_CLIENT_CERT_KEY: Kafka connection private key, optional, default value is undefined;
if not provided, then SSL connection is not used, direct insecure connection is used;
if provided, it can be either path to private key file or private key content
- KAFKA_TOPIC: Kafka topic to listen, default value is 'challenge.notification.create'
- CHALLENGE_CREATE_TOPIC: Kafka topic to listen, default value is 'challenge.notification.create'
- PROJECT_MEMBER_ADDED_TOPIC: Kafka topic to listen when a member is added to a project, default value: connect.notification.project.member.joined
- PROJECT_MEMBER_REMOVED_TOPIC: Kafka topic to listen when a member is removed to a project, default value: connect.notification.project.member.removed
- REQUEST_TIMEOUT: superagent request timeout in milliseconds, default value is 20000
- RESOURCE_ROLE_ID: the challenge member resource role id
- MANAGER_RESOURCE_ROLE_ID: the challenge manager resource role ID
- GET_PROJECT_API_BASE: get project API base URL, default value is mock API 'http://localhost:4000/v5/projects'
- SEARCH_MEMBERS_API_BASE: search members API base URL, default value is 'https://api.topcoder.com/v3/members/_search'
- CREATE_RESOURCE_API: create resource API URL, default value is mock API 'http://localhost:4000/v5/resources'
- RESOURCES_API: create resource API URL, default value is mock API 'http://localhost:4000/v5/resources'
- CHALLENGE_API: the challennge API URL, default value is http://localhost:4000/v5/challenges


Set the following environment variables so that the app can get TC M2M token (use 'set' insted of 'export' for Windows OS):
Expand Down
9 changes: 7 additions & 2 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ module.exports = {
// for the local Kafka, they are not needed
KAFKA_CLIENT_CERT: process.env.KAFKA_CLIENT_CERT,
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY,
KAFKA_TOPIC: process.env.KAFKA_TOPIC || 'challenge.notification.create',
// Topics
CHALLENGE_CREATE_TOPIC: process.env.CHALLENGE_CREATE_TOPIC || 'challenge.notification.create',
PROJECT_MEMBER_ADDED_TOPIC: process.env.PROJECT_MEMBER_ADDED_TOPIC || 'connect.notification.project.member.joined',
PROJECT_MEMBER_REMOVED_TOPIC: process.env.PROJECT_MEMBER_REMOVED_TOPIC || 'connect.notification.project.member.removed',

// superagent request timeout in milliseconds
REQUEST_TIMEOUT: process.env.REQUEST_TIMEOUT ? Number(process.env.REQUEST_TIMEOUT) : 20000,

RESOURCE_ROLE_ID: process.env.RESOURCE_ROLE_ID || '2a4dc376-a31c-4d00-b173-13934d89e286',
MANAGER_RESOURCE_ROLE_ID: process.env.MANAGER_RESOURCE_ROLE_ID || '0e9c6879-39e4-4eb6-b8df-92407890faf1',

GET_PROJECT_API_BASE: process.env.GET_PROJECT_API_BASE || 'http://localhost:4000/v5/projects',
SEARCH_MEMBERS_API_BASE: process.env.SEARCH_MEMBERS_API_BASE || 'https://api.topcoder-dev.com/v3/members/_search',
CREATE_RESOURCE_API: process.env.CREATE_RESOURCE_API || 'http://localhost:4000/v5/resources'
RESOURCES_API: process.env.RESOURCES_API || 'http://localhost:4000/v5/resources',
CHALLENGE_API: process.env.CHALLENGE_API || 'http://localhost:4000/v5/challenges'
}
2 changes: 1 addition & 1 deletion docs/dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
RESOURCE_ROLE_ID: '',
GET_PROJECT_API_BASE: '',
SEARCH_MEMBERS_API_BASE: '',
CREATE_RESOURCE_API: ''
RESOURCES_API: ''
}
2 changes: 1 addition & 1 deletion docs/prod.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
RESOURCE_ROLE_ID: '',
GET_PROJECT_API_BASE: '',
SEARCH_MEMBERS_API_BASE: '',
CREATE_RESOURCE_API: ''
RESOURCES_API: ''
}
15 changes: 13 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ const dataHandler = (messageSet, topic, partition) => Promise.each(messageSet, (
}

return (async () => {
await ProcessorService.processMessage(messageJSON)
if (topic === config.CHALLENGE_CREATE_TOPIC) {
await ProcessorService.handleChallengeCreate(messageJSON)
} else if (topic === config.PROJECT_MEMBER_ADDED_TOPIC) {
await ProcessorService.handleMemberAdded(messageJSON)
} else if (topic === config.PROJECT_MEMBER_REMOVED_TOPIC) {
await ProcessorService.handleMemberRemoved(messageJSON)
}
logger.debug('Successfully processed message')
})()
// commit offset
.then(() => consumer.commitOffset({ topic, partition, offset: m.offset }))
Expand All @@ -57,7 +64,11 @@ function check () {
logger.info('Starting kafka consumer')
consumer
.init([{
subscriptions: [config.KAFKA_TOPIC],
subscriptions: [
config.CHALLENGE_CREATE_TOPIC,
config.PROJECT_MEMBER_ADDED_TOPIC,
config.PROJECT_MEMBER_REMOVED_TOPIC
],
handler: dataHandler
}])
.then(() => {
Expand Down
103 changes: 99 additions & 4 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,75 @@ async function getProject (projectId) {
return res.body
}

/**
* Get all challenges for a specific project
* @param {Number} projectId the project ID
*/
async function getProjectChallenges (projectId) {
const token = await getM2MToken()
const url = `${config.CHALLENGE_API}`
let allChallenges = []
let page = 1
while (true) {
const res = await superagent
.get(url)
.query({
projectId,
isLightweight: true,
perPage: 100
})
.set('Authorization', `Bearer ${token}`)
if (res.status !== 200) {
throw new Error(`Failed to get project details of id ${projectId}: ${_.get(res.body, 'message')}`)
}
const challenges = res.body || []
if (challenges.length === 0) {
break
}
allChallenges = allChallenges.concat(_.map(challenges, c => _.pick(c, ['id'])))
page += 1
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
break
}
}
return allChallenges
}

/**
* Get challenge resources
* @param {String} challengeId the challenge ID
* @param {String} roleId the role ID
*/
async function getChallengeResources (challengeId, roleId) {
const token = await getM2MToken()
const url = `${config.RESOURCES_API}`
let allResources = []
let page = 1
while (true) {
const res = await superagent
.get(url)
.query({
challengeId,
roleId: roleId || config.RESOURCE_ROLE_ID,
perPage: 100
})
.set('Authorization', `Bearer ${token}`)
if (res.status !== 200) {
throw new Error(`Failed to get resources for challenge id ${challengeId}: ${_.get(res.body, 'message')}`)
}
const resources = res.body || []
if (resources.length === 0) {
break
}
allResources = allResources.concat(resources)
page += 1
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
break
}
}
return allResources
}

/**
* Search members of given member ids
* @param {Array} memberIds the member ids
Expand Down Expand Up @@ -79,18 +148,41 @@ async function searchMembers (memberIds) {
* Create resource.
* @param {String} challengeId the challenge id
* @param {String} memberHandle the member handle
* @param {String} roleId the role ID
* @return {Object} the created resource
*/
async function createResource (challengeId, memberHandle, roleId) {
// M2M token is cached by 'tc-core-library-js' lib
const token = await getM2MToken()
const res = await superagent
.post(config.RESOURCES_API)
.set('Authorization', `Bearer ${token}`)
.send({
challengeId,
memberHandle,
roleId: roleId || config.RESOURCE_ROLE_ID
})
.timeout(config.REQUEST_TIMEOUT)
return res.body
}

/**
* Delete resource.
* @param {String} challengeId the challenge id
* @param {String} memberHandle the member handle
* @param {String} roleId the role ID
* @return {Object} the created resource
*/
async function createResource (challengeId, memberHandle) {
async function deleteResource (challengeId, memberHandle, roleId) {
// M2M token is cached by 'tc-core-library-js' lib
const token = await getM2MToken()
const res = await superagent
.post(config.CREATE_RESOURCE_API)
.delete(config.RESOURCES_API)
.set('Authorization', `Bearer ${token}`)
.send({
challengeId,
memberHandle,
roleId: config.RESOURCE_ROLE_ID
roleId: roleId || config.RESOURCE_ROLE_ID
})
.timeout(config.REQUEST_TIMEOUT)
return res.body
Expand All @@ -100,5 +192,8 @@ module.exports = {
getKafkaOptions,
getProject,
searchMembers,
createResource
createResource,
deleteResource,
getProjectChallenges,
getChallengeResources
}
80 changes: 77 additions & 3 deletions src/services/ProcessorService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

const _ = require('lodash')
const Joi = require('joi')
const config = require('config')
const logger = require('../common/logger')
const helper = require('../common/helper')

/**
* Process Kafka message of challenge created
* @param {Object} message the challenge created message
*/
async function processMessage (message) {
async function handleChallengeCreate (message) {
const challengeId = message.payload.id
const projectId = message.payload.projectId
logger.info(`Process message of challenge id ${challengeId} and project id ${projectId}`)
Expand All @@ -32,7 +33,7 @@ async function processMessage (message) {
logger.info(`Successfully processed message of challenge id ${challengeId} and project id ${projectId}`)
}

processMessage.schema = {
handleChallengeCreate.schema = {
message: Joi.object().keys({
topic: Joi.string().required(),
originator: Joi.string().required(),
Expand All @@ -45,9 +46,82 @@ processMessage.schema = {
}).required()
}

/**
* Handle project member changes
* @param {Number} projectId the project ID
* @param {Number} userId the user ID
* @param {Boolean} isDeleted flag to indicate that a member has been deleted
*/
async function handleProjectMemberChange (projectId, userId, isDeleted) {
// verify project exists
await helper.getProject(projectId)
// get project challenges
const challenges = await helper.getProjectChallenges(projectId)
// get member handle
const [memberDetails] = await helper.searchMembers([userId])
const { handle } = memberDetails
for (const challenge of challenges) {
const challenngeResources = await helper.getChallengeResources(challenge.id, config.MANAGER_RESOURCE_ROLE_ID)
const existing = _.find(challenngeResources, r => _.toString(r.memberId) === _.toString(userId))
if (isDeleted) {
if (existing) {
await helper.deleteResource(challenge.id, handle, config.MANAGER_RESOURCE_ROLE_ID)
}
} else {
if (!existing) {
await helper.createResource(challenge.id, handle, config.MANAGER_RESOURCE_ROLE_ID)
}
}
}
}

/**
* Process kafka message of member added to a project
* @param {Object} message the member added message
*/
async function handleMemberAdded (message) {
await handleProjectMemberChange(message.payload.projectId, message.payload.userId)
}

handleMemberAdded.schema = {
message: Joi.object().keys({
topic: Joi.string().required(),
originator: Joi.string().required(),
timestamp: Joi.date().required(),
'mime-type': Joi.string().required(),
payload: Joi.object().keys({
projectId: Joi.number().integer().positive().required(),
userId: Joi.number().integer().positive().required()
}).unknown(true).required()
}).required()
}

/**
* Process kafka message of member removed to a project
* @param {Object} message the member added message
*/
async function handleMemberRemoved (message) {
await handleProjectMemberChange(message.payload.projectId, message.payload.userId, true)
}

handleMemberRemoved.schema = {
message: Joi.object().keys({
topic: Joi.string().required(),
originator: Joi.string().required(),
timestamp: Joi.date().required(),
'mime-type': Joi.string().required(),
payload: Joi.object().keys({
projectId: Joi.number().integer().positive().required(),
userId: Joi.number().integer().positive().required()
}).unknown(true).required()
}).required()
}

// Exports
module.exports = {
processMessage
handleChallengeCreate,
handleMemberAdded,
handleMemberRemoved
}

logger.buildService(module.exports)
Loading