Skip to content

Commit a0a788b

Browse files
committed
migration to v5 API
1 parent 31a8527 commit a0a788b

10 files changed

+408
-216
lines changed

config/default.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ module.exports = {
2121
BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5',
2222
KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'error.notification',
2323
KAFKA_AGGREGATE_TOPIC: process.env.KAFKA_AGGREGATE_TOPIC || 'submission.notification.aggregate',
24-
CHALLENGEAPI_URL: process.env.CHALLENGEAPI_URL || 'https://api.topcoder-dev.com/v4/challenges',
2524
CHALLENGEAPI_V5_URL: process.env.CHALLENGEAPI_V5_URL || 'https://api.topcoder-dev.com/v5/challenges',
25+
RESOURCEAPI_V5_BASE_URL: process.env.RESOURCEAPI_V5_BASE_URL || 'https://api.topcoder-dev.com/v5',
2626
AUTH0_URL: process.env.AUTH0_URL, // Auth0 credentials for Submission Service
2727
AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder.com',
2828
TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME,

config/test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = {
77
LOG_LEVEL: 'info',
88
WEB_SERVER_PORT: 3010,
99
AUTH_SECRET: 'mysecret',
10-
VALID_ISSUERS: '["https://api.topcoder.com"]',
10+
VALID_ISSUERS: process.env.VALID_ISSUERS ? process.env.VALID_ISSUERS.replace(/\\"/g, '') : '["https://api.topcoder.com","https://topcoder-dev.auth0.com/"]',
1111
API_VERSION: process.env.API_VERSION || '/api/v5',
1212
aws: {
1313
AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used by the application
@@ -16,14 +16,14 @@ module.exports = {
1616
S3_BUCKET: process.env.S3_BUCKET_TEST || 'tc-testing-submissions' // S3 Bucket to which submissions need to be uploaded
1717
},
1818
BUSAPI_EVENTS_URL: 'https://api.topcoder-dev.com/v5/bus/events',
19-
CHALLENGEAPI_URL: 'https://api.topcoder-dev.com/v4/challenges',
19+
BUSAPI_URL: 'https://api.topcoder-dev.com/v5',
20+
CHALLENGEAPI_V5_URL: 'https://api.topcoder-dev.com/v5/challenges',
2021
esConfig: {
2122
ES_INDEX: process.env.ES_INDEX_TEST || 'submission-test',
2223
ES_TYPE: process.env.ES_TYPE_TEST || '_doc' // ES 6.x accepts only 1 Type per index and it's mandatory to define it
2324
},
2425
AUTH0_URL: process.env.AUTH0_URL, // Auth0 credentials for Submission Service
25-
AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder.com',
26-
TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME,
26+
AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE,
2727
AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
2828
AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
2929
USER_TOKEN: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6IlNoYXJhdGhrdW1hcjkyIiwiZXhwIjo1NTUzMDE5OTI1OSwidXNlcklkIjoiNDA0OTMwNTAiLCJpYXQiOjE1MzAxOTg2NTksImVtYWlsIjoiU2hhcmF0aGt1bWFyOTJAdG9wY29kZXIuY29tIiwianRpIjoiYzNhYzYwOGEtNTZiZS00NWQwLThmNmEtMzFmZTk0Yjk1NjFjIn0.2gtNJwhcv7MYc-muX3Nv-B0RdWbhMRl7-xrwFUsLazM',

src/common/helper.js

+121-59
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function autoWrapExpress (obj) {
5353
return obj
5454
}
5555
_.each(obj, (value, key) => {
56-
obj[key] = autoWrapExpress(value); //eslint-disable-line
56+
obj[key] = autoWrapExpress(value); //eslint-disable-line
5757
})
5858
return obj
5959
}
@@ -299,30 +299,6 @@ function * getM2Mtoken () {
299299
return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
300300
}
301301

302-
/**
303-
* Get legacy challenge id if the challenge id is uuid form
304-
* @param {String} challengeId Challenge ID
305-
* @returns {String} Legacy Challenge ID of the given challengeId
306-
*/
307-
function * getLegacyChallengeId (challengeId) {
308-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) {
309-
logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`)
310-
const token = yield getM2Mtoken()
311-
try {
312-
const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
313-
.set('Authorization', `Bearer ${token}`)
314-
.set('Content-Type', 'application/json')
315-
const legacyId = parseInt(response.body.legacyId, 10)
316-
logger.debug(`Legacy challenge id is ${legacyId} for v5 challenge id ${challengeId}`)
317-
return legacyId
318-
} catch (err) {
319-
logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
320-
throw err
321-
}
322-
}
323-
return challengeId
324-
}
325-
326302
/*
327303
* Get submission phase ID of a challenge from Challenge API
328304
* @param challengeId Challenge ID
@@ -335,20 +311,20 @@ function * getSubmissionPhaseId (challengeId) {
335311
try {
336312
logger.info(`Calling to challenge API to find submission phase Id for ${challengeId}`)
337313
const token = yield getM2Mtoken()
338-
response = yield request.get(`${config.CHALLENGEAPI_URL}/${challengeId}/phases`)
314+
response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
339315
.set('Authorization', `Bearer ${token}`)
340316
.set('Content-Type', 'application/json')
341317
logger.info(`returned from finding submission phase Id for ${challengeId}`)
342318
} catch (ex) {
343-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${challengeId}/phases`)
319+
logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`)
344320
logger.debug('Setting submissionPhaseId to Null')
345321
response = null
346322
}
347323
if (response) {
348-
const phases = _.get(response.body, 'result.content', [])
349-
const checkPoint = _.filter(phases, { phaseType: 'Checkpoint Submission', phaseStatus: 'Open' })
350-
const submissionPh = _.filter(phases, { phaseType: 'Submission', phaseStatus: 'Open' })
351-
const finalFixPh = _.filter(phases, { phaseType: 'Final Fix', phaseStatus: 'Open' })
324+
const phases = _.get(response.body, 'phases', [])
325+
const checkPoint = _.filter(phases, { name: 'Checkpoint Submission', isOpen: true })
326+
const submissionPh = _.filter(phases, { name: 'Submission', isOpen: true })
327+
const finalFixPh = _.filter(phases, { name: 'Final Fix', isOpen: true })
352328
if (checkPoint.length !== 0) {
353329
phaseId = checkPoint[0].id
354330
} else if (submissionPh.length !== 0) {
@@ -379,31 +355,45 @@ function * checkCreateAccess (authUser, subEntity) {
379355

380356
try {
381357
logger.info(`Calling to challenge API for fetch phases and winners for ${subEntity.challengeId}`)
382-
challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`)
358+
challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`)
383359
.set('Authorization', `Bearer ${token}`)
384360
.set('Content-Type', 'application/json')
385361
logger.info(`returned for ${subEntity.challengeId} with ${JSON.stringify(challengeDetails)}`)
386362
} catch (ex) {
387-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`)
363+
logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`)
388364
logger.error(ex)
389365
throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${subEntity.challengeId}`)
390366
}
391367

392368
try {
393-
resources = yield request.get(`${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`)
369+
resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`)
394370
.set('Authorization', `Bearer ${token}`)
395371
.set('Content-Type', 'application/json')
396372
} catch (ex) {
397-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`)
373+
logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`)
398374
logger.error(ex)
399375
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`)
400376
}
401377

378+
// Get map of role id to role name
379+
const resourceRolesMap = yield getRoleIdToRoleNameMap()
380+
381+
// Check if role id to role name mapping is available. If not user's role cannot be determined.
382+
if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) {
383+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`)
384+
}
385+
402386
if (resources && challengeDetails) {
403-
const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } })
387+
const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle })
388+
389+
// Populate the role names for the current user role ids
390+
_.forEach(currUserRoles, currentUserRole => {
391+
currentUserRole.role = resourceRolesMap[currentUserRole.roleId]
392+
})
393+
404394
// Get phases and winner detail from challengeDetails
405-
const phases = challengeDetails.body.result.content[0].allPhases
406-
const winner = challengeDetails.body.result.content[0].winners
395+
const phases = challengeDetails.body.phases
396+
const winner = challengeDetails.body.winners
407397

408398
// Check if the User is registered for the contest
409399
const submitters = _.filter(currUserRoles, { role: 'Submitter' })
@@ -419,7 +409,7 @@ function * checkCreateAccess (authUser, subEntity) {
419409

420410
const currPhase = _.filter(phases, { id: submissionPhaseId })
421411

422-
if (currPhase[0].phaseType === 'Final Fix') {
412+
if (currPhase[0].name === 'Final Fix') {
423413
if (!authUser.handle.equals(winner[0].handle)) {
424414
throw new errors.HttpStatusError(403, 'Only winner is allowed to submit during Final Fix phase')
425415
}
@@ -448,30 +438,43 @@ function * checkGetAccess (authUser, submission) {
448438
const token = yield getM2Mtoken()
449439

450440
try {
451-
resources = yield request.get(`${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`)
441+
resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`)
452442
.set('Authorization', `Bearer ${token}`)
453443
.set('Content-Type', 'application/json')
454444
} catch (ex) {
455-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`)
445+
logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`)
456446
logger.error(ex)
457447
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`)
458448
}
459449

460450
try {
461-
challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`)
451+
challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`)
462452
.set('Authorization', `Bearer ${token}`)
463453
.set('Content-Type', 'application/json')
464454
} catch (ex) {
465-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`)
455+
logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`)
466456
logger.error(ex)
467457
throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${submission.challengeId}`)
468458
}
469459

460+
// Get map of role id to role name
461+
const resourceRolesMap = yield getRoleIdToRoleNameMap()
462+
463+
// Check if role id to role name mapping is available. If not user's role cannot be determined.
464+
if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) {
465+
throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`)
466+
}
467+
470468
if (resources && challengeDetails) {
471469
// Fetch all roles of the User pertaining to the current challenge
472-
const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } })
473-
const subTrack = challengeDetails.body.result.content[0].subTrack
474-
const phases = challengeDetails.body.result.content[0].allPhases
470+
const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle })
471+
472+
// Populate the role names for the current user role ids
473+
_.forEach(currUserRoles, currentUserRole => {
474+
currentUserRole.role = resourceRolesMap[currentUserRole.roleId]
475+
})
476+
477+
const subTrack = challengeDetails.body.legacy.subTrack
475478

476479
// Check if the User is a Copilot
477480
const copilot = _.filter(currUserRoles, { role: 'Copilot' })
@@ -494,18 +497,18 @@ function * checkGetAccess (authUser, submission) {
494497

495498
// User is either a Reviewer or Screener
496499
if (screener.length !== 0 || reviewer.length !== 0) {
497-
const screeningPhase = _.filter(phases, { phaseType: 'Screening', phaseStatus: 'Scheduled' })
498-
const reviewPhase = _.filter(phases, { phaseType: 'Review', phaseStatus: 'Scheduled' })
500+
const screeningPhaseStatus = getPhaseStatus('Screening', challengeDetails.body)
501+
const reviewPhaseStatus = getPhaseStatus('Review', challengeDetails.body)
499502

500503
// Neither Screening Nor Review is Opened / Closed
501-
if (screeningPhase.length !== 0 && reviewPhase.length !== 0) {
504+
if (screeningPhaseStatus === 'Scheduled' && reviewPhaseStatus === 'Scheduled') {
502505
throw new errors.HttpStatusError(403, 'You can access the submission only when Screening / Review is open')
503506
}
504507
} else {
505-
const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' })
508+
const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body)
506509

507510
// Appeals Response is not closed yet
508-
if (appealsResponse.length === 0) {
511+
if (appealsResponseStatus !== 'Closed') {
509512
throw new errors.HttpStatusError(403, 'You cannot access other submissions before the end of Appeals Response phase')
510513
} else {
511514
const userSubmission = yield fetchFromES({
@@ -545,28 +548,27 @@ function * checkReviewGetAccess (authUser, submission) {
545548
const token = yield getM2Mtoken()
546549

547550
try {
548-
challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`)
551+
challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`)
549552
.set('Authorization', `Bearer ${token}`)
550553
.set('Content-Type', 'application/json')
551554
} catch (ex) {
552-
logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`)
555+
logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`)
553556
logger.error(ex)
554557
return false
555558
}
556559

557560
if (challengeDetails) {
558-
const subTrack = challengeDetails.body.result.content[0].subTrack
559-
const phases = challengeDetails.body.result.content[0].allPhases
561+
const subTrack = challengeDetails.body.legacy.subTrack
560562

561563
// For Marathon Match, everyone can access review result
562564
if (subTrack === 'DEVELOP_MARATHON_MATCH') {
563565
logger.info('No access check for Marathon match')
564566
return true
565567
} else {
566-
const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' })
568+
const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body)
567569

568570
// Appeals Response is not closed yet
569-
if (appealsResponse.length === 0) {
571+
if (appealsResponseStatus !== 'Closed') {
570572
throw new errors.HttpStatusError(403, 'You cannot access the review before the end of the Appeals Response phase')
571573
}
572574

@@ -642,19 +644,79 @@ function cleanseReviews (reviews, authUser) {
642644
return reviews
643645
}
644646

647+
/**
648+
* Function to get role id to role name map
649+
* @returns {Object|null} <Role Id, Role Name> map
650+
*/
651+
function * getRoleIdToRoleNameMap () {
652+
let resourceRoles
653+
let resourceRolesMap = null
654+
const token = yield getM2Mtoken()
655+
try {
656+
resourceRoles = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`)
657+
.set('Authorization', `Bearer ${token}`)
658+
.set('Content-Type', 'application/json')
659+
} catch (ex) {
660+
logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`)
661+
logger.error(ex)
662+
resourceRoles = null
663+
}
664+
if (resourceRoles) {
665+
resourceRolesMap = {}
666+
_.forEach(resourceRoles.body, resourceRole => {
667+
resourceRolesMap[resourceRole.id] = resourceRole.name
668+
})
669+
}
670+
return resourceRolesMap
671+
}
672+
673+
/**
674+
* Function to get phase status of phases used in an active challenge
675+
* @param {String} phaseName the phase name for retrieving status
676+
* @param {Object} challengeDetails the challenge details
677+
* @returns {('Scheduled' | 'Open' | 'Closed' | 'Invalid')} status of the phase
678+
*/
679+
function getPhaseStatus (phaseName, challengeDetails) {
680+
const { phases } = challengeDetails
681+
const queriedPhaseIndex = _.findIndex(phases, phase => {
682+
return phase.name === phaseName
683+
})
684+
// Requested phase name could not be found in phases hence 'Invalid'
685+
if (queriedPhaseIndex === -1) {
686+
return 'Invalid'
687+
}
688+
// If requested phase name is open return 'Open'
689+
if (phases[queriedPhaseIndex].isOpen) {
690+
return 'Open'
691+
} else {
692+
const { actualEndDate } = phases[queriedPhaseIndex]
693+
if (!_.isEmpty(actualEndDate)) {
694+
const present = new Date().getTime()
695+
const actualDate = new Date(actualEndDate).getTime()
696+
if (present > actualDate) {
697+
return 'Closed'
698+
} else {
699+
return 'Scheduled'
700+
}
701+
} else {
702+
return 'Scheduled'
703+
}
704+
}
705+
}
706+
645707
module.exports = {
646708
wrapExpress,
647709
autoWrapExpress,
648710
getEsClient,
649711
fetchFromES,
650712
camelize,
651713
setPaginationHeaders,
652-
getLegacyChallengeId,
653714
getSubmissionPhaseId,
654715
checkCreateAccess,
655716
checkGetAccess,
656717
checkReviewGetAccess,
657718
downloadFile,
658719
postToBusApi,
659-
cleanseReviews
720+
cleanseReviews,
721+
getRoleIdToRoleNameMap
660722
}

0 commit comments

Comments
 (0)