Skip to content

Commit 740cdec

Browse files
authored
Merge pull request #11 from topcoder-platform/user-access-updates
user access updates
2 parents 22eb2e7 + 183399c commit 740cdec

File tree

8 files changed

+213
-24
lines changed

8 files changed

+213
-24
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,16 @@ The following parameters can be set in config files or in env variables:
4545
- KAFKA_CLIENT_CERT_KEY: Kafka connection private key, optional, default value is undefined;
4646
if not provided, then SSL connection is not used, direct insecure connection is used;
4747
if provided, it can be either path to private key file or private key content
48-
- KAFKA_TOPIC: Kafka topic to listen, default value is 'challenge.notification.create'
48+
- CHALLENGE_CREATE_TOPIC: Kafka topic to listen, default value is 'challenge.notification.create'
49+
- PROJECT_MEMBER_ADDED_TOPIC: Kafka topic to listen when a member is added to a project, default value: connect.notification.project.member.joined
50+
- PROJECT_MEMBER_REMOVED_TOPIC: Kafka topic to listen when a member is removed to a project, default value: connect.notification.project.member.removed
4951
- REQUEST_TIMEOUT: superagent request timeout in milliseconds, default value is 20000
5052
- RESOURCE_ROLE_ID: the challenge member resource role id
53+
- MANAGER_RESOURCE_ROLE_ID: the challenge manager resource role ID
5154
- GET_PROJECT_API_BASE: get project API base URL, default value is mock API 'http://localhost:4000/v5/projects'
5255
- SEARCH_MEMBERS_API_BASE: search members API base URL, default value is 'https://api.topcoder.com/v3/members/_search'
53-
- CREATE_RESOURCE_API: create resource API URL, default value is mock API 'http://localhost:4000/v5/resources'
56+
- RESOURCES_API: create resource API URL, default value is mock API 'http://localhost:4000/v5/resources'
57+
- CHALLENGE_API: the challennge API URL, default value is http://localhost:4000/v5/challenges
5458

5559

5660
Set the following environment variables so that the app can get TC M2M token (use 'set' insted of 'export' for Windows OS):

config/default.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ module.exports = {
2121
// for the local Kafka, they are not needed
2222
KAFKA_CLIENT_CERT: process.env.KAFKA_CLIENT_CERT,
2323
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY,
24-
KAFKA_TOPIC: process.env.KAFKA_TOPIC || 'challenge.notification.create',
24+
// Topics
25+
CHALLENGE_CREATE_TOPIC: process.env.CHALLENGE_CREATE_TOPIC || 'challenge.notification.create',
26+
PROJECT_MEMBER_ADDED_TOPIC: process.env.PROJECT_MEMBER_ADDED_TOPIC || 'connect.notification.project.member.joined',
27+
PROJECT_MEMBER_REMOVED_TOPIC: process.env.PROJECT_MEMBER_REMOVED_TOPIC || 'connect.notification.project.member.removed',
2528

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

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

3135
GET_PROJECT_API_BASE: process.env.GET_PROJECT_API_BASE || 'http://localhost:4000/v5/projects',
3236
SEARCH_MEMBERS_API_BASE: process.env.SEARCH_MEMBERS_API_BASE || 'https://api.topcoder-dev.com/v3/members/_search',
33-
CREATE_RESOURCE_API: process.env.CREATE_RESOURCE_API || 'http://localhost:4000/v5/resources'
37+
RESOURCES_API: process.env.RESOURCES_API || 'http://localhost:4000/v5/resources',
38+
CHALLENGE_API: process.env.CHALLENGE_API || 'http://localhost:4000/v5/challenges'
3439
}

docs/dev.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
RESOURCE_ROLE_ID: '',
2424
GET_PROJECT_API_BASE: '',
2525
SEARCH_MEMBERS_API_BASE: '',
26-
CREATE_RESOURCE_API: ''
26+
RESOURCES_API: ''
2727
}

docs/prod.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
RESOURCE_ROLE_ID: '',
2424
GET_PROJECT_API_BASE: '',
2525
SEARCH_MEMBERS_API_BASE: '',
26-
CREATE_RESOURCE_API: ''
26+
RESOURCES_API: ''
2727
}

src/app.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ const dataHandler = (messageSet, topic, partition) => Promise.each(messageSet, (
3434
}
3535

3636
return (async () => {
37-
await ProcessorService.processMessage(messageJSON)
37+
if (topic === config.CHALLENGE_CREATE_TOPIC) {
38+
await ProcessorService.handleChallengeCreate(messageJSON)
39+
} else if (topic === config.PROJECT_MEMBER_ADDED_TOPIC) {
40+
await ProcessorService.handleMemberAdded(messageJSON)
41+
} else if (topic === config.PROJECT_MEMBER_REMOVED_TOPIC) {
42+
await ProcessorService.handleMemberRemoved(messageJSON)
43+
}
44+
logger.debug('Successfully processed message')
3845
})()
3946
// commit offset
4047
.then(() => consumer.commitOffset({ topic, partition, offset: m.offset }))
@@ -57,7 +64,11 @@ function check () {
5764
logger.info('Starting kafka consumer')
5865
consumer
5966
.init([{
60-
subscriptions: [config.KAFKA_TOPIC],
67+
subscriptions: [
68+
config.CHALLENGE_CREATE_TOPIC,
69+
config.PROJECT_MEMBER_ADDED_TOPIC,
70+
config.PROJECT_MEMBER_REMOVED_TOPIC
71+
],
6172
handler: dataHandler
6273
}])
6374
.then(() => {

src/common/helper.js

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,75 @@ async function getProject (projectId) {
4747
return res.body
4848
}
4949

50+
/**
51+
* Get all challenges for a specific project
52+
* @param {Number} projectId the project ID
53+
*/
54+
async function getProjectChallenges (projectId) {
55+
const token = await getM2MToken()
56+
const url = `${config.CHALLENGE_API}`
57+
let allChallenges = []
58+
let page = 1
59+
while (true) {
60+
const res = await superagent
61+
.get(url)
62+
.query({
63+
projectId,
64+
isLightweight: true,
65+
perPage: 100
66+
})
67+
.set('Authorization', `Bearer ${token}`)
68+
if (res.status !== 200) {
69+
throw new Error(`Failed to get project details of id ${projectId}: ${_.get(res.body, 'message')}`)
70+
}
71+
const challenges = res.body || []
72+
if (challenges.length === 0) {
73+
break
74+
}
75+
allChallenges = allChallenges.concat(_.map(challenges, c => _.pick(c, ['id'])))
76+
page += 1
77+
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
78+
break
79+
}
80+
}
81+
return allChallenges
82+
}
83+
84+
/**
85+
* Get challenge resources
86+
* @param {String} challengeId the challenge ID
87+
* @param {String} roleId the role ID
88+
*/
89+
async function getChallengeResources (challengeId, roleId) {
90+
const token = await getM2MToken()
91+
const url = `${config.RESOURCES_API}`
92+
let allResources = []
93+
let page = 1
94+
while (true) {
95+
const res = await superagent
96+
.get(url)
97+
.query({
98+
challengeId,
99+
roleId: roleId || config.RESOURCE_ROLE_ID,
100+
perPage: 100
101+
})
102+
.set('Authorization', `Bearer ${token}`)
103+
if (res.status !== 200) {
104+
throw new Error(`Failed to get resources for challenge id ${challengeId}: ${_.get(res.body, 'message')}`)
105+
}
106+
const resources = res.body || []
107+
if (resources.length === 0) {
108+
break
109+
}
110+
allResources = allResources.concat(resources)
111+
page += 1
112+
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
113+
break
114+
}
115+
}
116+
return allResources
117+
}
118+
50119
/**
51120
* Search members of given member ids
52121
* @param {Array} memberIds the member ids
@@ -79,18 +148,41 @@ async function searchMembers (memberIds) {
79148
* Create resource.
80149
* @param {String} challengeId the challenge id
81150
* @param {String} memberHandle the member handle
151+
* @param {String} roleId the role ID
152+
* @return {Object} the created resource
153+
*/
154+
async function createResource (challengeId, memberHandle, roleId) {
155+
// M2M token is cached by 'tc-core-library-js' lib
156+
const token = await getM2MToken()
157+
const res = await superagent
158+
.post(config.RESOURCES_API)
159+
.set('Authorization', `Bearer ${token}`)
160+
.send({
161+
challengeId,
162+
memberHandle,
163+
roleId: roleId || config.RESOURCE_ROLE_ID
164+
})
165+
.timeout(config.REQUEST_TIMEOUT)
166+
return res.body
167+
}
168+
169+
/**
170+
* Delete resource.
171+
* @param {String} challengeId the challenge id
172+
* @param {String} memberHandle the member handle
173+
* @param {String} roleId the role ID
82174
* @return {Object} the created resource
83175
*/
84-
async function createResource (challengeId, memberHandle) {
176+
async function deleteResource (challengeId, memberHandle, roleId) {
85177
// M2M token is cached by 'tc-core-library-js' lib
86178
const token = await getM2MToken()
87179
const res = await superagent
88-
.post(config.CREATE_RESOURCE_API)
180+
.delete(config.RESOURCES_API)
89181
.set('Authorization', `Bearer ${token}`)
90182
.send({
91183
challengeId,
92184
memberHandle,
93-
roleId: config.RESOURCE_ROLE_ID
185+
roleId: roleId || config.RESOURCE_ROLE_ID
94186
})
95187
.timeout(config.REQUEST_TIMEOUT)
96188
return res.body
@@ -100,5 +192,8 @@ module.exports = {
100192
getKafkaOptions,
101193
getProject,
102194
searchMembers,
103-
createResource
195+
createResource,
196+
deleteResource,
197+
getProjectChallenges,
198+
getChallengeResources
104199
}

src/services/ProcessorService.js

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
const _ = require('lodash')
66
const Joi = require('joi')
7+
const config = require('config')
78
const logger = require('../common/logger')
89
const helper = require('../common/helper')
910

1011
/**
1112
* Process Kafka message of challenge created
1213
* @param {Object} message the challenge created message
1314
*/
14-
async function processMessage (message) {
15+
async function handleChallengeCreate (message) {
1516
const challengeId = message.payload.id
1617
const projectId = message.payload.projectId
1718
logger.info(`Process message of challenge id ${challengeId} and project id ${projectId}`)
@@ -32,7 +33,7 @@ async function processMessage (message) {
3233
logger.info(`Successfully processed message of challenge id ${challengeId} and project id ${projectId}`)
3334
}
3435

35-
processMessage.schema = {
36+
handleChallengeCreate.schema = {
3637
message: Joi.object().keys({
3738
topic: Joi.string().required(),
3839
originator: Joi.string().required(),
@@ -45,9 +46,82 @@ processMessage.schema = {
4546
}).required()
4647
}
4748

49+
/**
50+
* Handle project member changes
51+
* @param {Number} projectId the project ID
52+
* @param {Number} userId the user ID
53+
* @param {Boolean} isDeleted flag to indicate that a member has been deleted
54+
*/
55+
async function handleProjectMemberChange (projectId, userId, isDeleted) {
56+
// verify project exists
57+
await helper.getProject(projectId)
58+
// get project challenges
59+
const challenges = await helper.getProjectChallenges(projectId)
60+
// get member handle
61+
const [memberDetails] = await helper.searchMembers([userId])
62+
const { handle } = memberDetails
63+
for (const challenge of challenges) {
64+
const challenngeResources = await helper.getChallengeResources(challenge.id, config.MANAGER_RESOURCE_ROLE_ID)
65+
const existing = _.find(challenngeResources, r => _.toString(r.memberId) === _.toString(userId))
66+
if (isDeleted) {
67+
if (existing) {
68+
await helper.deleteResource(challenge.id, handle, config.MANAGER_RESOURCE_ROLE_ID)
69+
}
70+
} else {
71+
if (!existing) {
72+
await helper.createResource(challenge.id, handle, config.MANAGER_RESOURCE_ROLE_ID)
73+
}
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Process kafka message of member added to a project
80+
* @param {Object} message the member added message
81+
*/
82+
async function handleMemberAdded (message) {
83+
await handleProjectMemberChange(message.payload.projectId, message.payload.userId)
84+
}
85+
86+
handleMemberAdded.schema = {
87+
message: Joi.object().keys({
88+
topic: Joi.string().required(),
89+
originator: Joi.string().required(),
90+
timestamp: Joi.date().required(),
91+
'mime-type': Joi.string().required(),
92+
payload: Joi.object().keys({
93+
projectId: Joi.number().integer().positive().required(),
94+
userId: Joi.number().integer().positive().required()
95+
}).unknown(true).required()
96+
}).required()
97+
}
98+
99+
/**
100+
* Process kafka message of member removed to a project
101+
* @param {Object} message the member added message
102+
*/
103+
async function handleMemberRemoved (message) {
104+
await handleProjectMemberChange(message.payload.projectId, message.payload.userId, true)
105+
}
106+
107+
handleMemberRemoved.schema = {
108+
message: Joi.object().keys({
109+
topic: Joi.string().required(),
110+
originator: Joi.string().required(),
111+
timestamp: Joi.date().required(),
112+
'mime-type': Joi.string().required(),
113+
payload: Joi.object().keys({
114+
projectId: Joi.number().integer().positive().required(),
115+
userId: Joi.number().integer().positive().required()
116+
}).unknown(true).required()
117+
}).required()
118+
}
119+
48120
// Exports
49121
module.exports = {
50-
processMessage
122+
handleChallengeCreate,
123+
handleMemberAdded,
124+
handleMemberRemoved
51125
}
52126

53127
logger.buildService(module.exports)

0 commit comments

Comments
 (0)