diff --git a/README.md b/README.md index 98818de2..13f5f4ea 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,11 @@ There are two parts need to be updated for local development - https://github.com/topcoder-platform/challenge-api/blob/develop/config/default.js#L27-L28 Two aws config should be uncommented +and AUTH0 related configuration must be set at config file or in env variables. +### Deploy the app + +- Follow the Notes section above - Install dependencies `npm install` - Run lint `npm run lint` - Run lint fix `npm run lint:fix` @@ -118,6 +122,7 @@ Two aws config should be uncommented or re-create the index: `npm run init-es force` - Create tables `npm run create-tables` - Clear and init db `npm run init-db` +- Seed tables: `npm run seed-tables` - Start app `npm start` - App is running at `http://localhost:3000` diff --git a/app-constants.js b/app-constants.js index d1b37094..d2b78ef1 100644 --- a/app-constants.js +++ b/app-constants.js @@ -55,6 +55,7 @@ const DiscussionTypes = { const Topics = { ChallengeCreated: 'challenge.notification.create', ChallengeUpdated: 'challenge.notification.update', + ChallengeDeleted: 'challenge.notification.delete', ChallengeTypeCreated: 'test.new.bus.events', // 'challenge.action.type.created', ChallengeTypeUpdated: 'test.new.bus.events', // 'challenge.action.type.updated', ChallengePhaseCreated: 'test.new.bus.events', // 'challenge.action.phase.created', diff --git a/config/default.js b/config/default.js index e4f1b17b..070efebc 100644 --- a/config/default.js +++ b/config/default.js @@ -25,8 +25,8 @@ module.exports = { KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'common.error.reporting', AMAZON: { - // AWS_ACCESS_KEY_ID: process.env.AWS_FAKE_ID, - // AWS_SECRET_ACCESS_KEY: process.env.AWS_FAKE_KEY, + // AWS_ACCESS_KEY_ID: process.env.AWS_FAKE_ID || "FAKE_ACCESS_KEY", + // AWS_SECRET_ACCESS_KEY: process.env.AWS_FAKE_KEY || "FAKE_SECRET_ACCESS_KEY", AWS_REGION: process.env.AWS_REGION || 'ap-northeast-1', IS_LOCAL_DB: process.env.IS_LOCAL_DB || true, DYNAMODB_URL: process.env.DYNAMODB_URL || 'http://localhost:8000', diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bedd977f..2c89fb87 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -605,6 +605,50 @@ paths: description: Server error schema: $ref: '#/definitions/ErrorModel' + delete: + tags: + - Challenges + description: > + Delete the challenge with the provided id. + Only challenges with status of "New" can be deleted. + security: + - bearer: [] + produces: + - application/json + parameters: + - name: challengeId + in: path + required: true + type: string + format: UUID + description: The id of challenge to update + responses: + '200': + description: Deleted - The request was successful and the resource is returned. + schema: + $ref: '#/definitions/Challenge' + '400': + description: Bad request. Request parameters were invalid. + schema: + $ref: '#/definitions/ErrorModel' + '401': + description: Unauthorized. Fail to authenticate the requester. + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: > + Forbidden. The requester does not have the correct permission to + update the challenge. + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Challenge not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Server error + schema: + $ref: '#/definitions/ErrorModel' /challenge-types: get: tags: @@ -2433,6 +2477,9 @@ definitions: numOfRegistrants: type: integer description: number of registrants + overview: + description: the overview of the challenge + type: object created: type: string format: date-time @@ -2903,6 +2950,9 @@ definitions: numOfRegistrants: type: integer description: number of registrants + overview: + description: the overview of the challenge + type: object created: type: string format: date-time diff --git a/docs/topcoder-challenge-api.postman_collection.json b/docs/topcoder-challenge-api.postman_collection.json index b4df3c8f..ba52f92b 100644 --- a/docs/topcoder-challenge-api.postman_collection.json +++ b/docs/topcoder-challenge-api.postman_collection.json @@ -20343,6 +20343,516 @@ ], "protocolProfileBehavior": {}, "_postman_isSubFolder": true + }, + { + "name": "delete challenge", + "item": [ + { + "name": "[STUB] CHALLENGE_ID1 can be set by running \"create challenge by admin\"", + "request": { + "method": "VIEW", + "header": [], + "url": { + "raw": "" + } + }, + "response": [] + }, + { + "name": "[STUB] CHALLENGE_ID2 can be set by running \"create challenge by copilot\"", + "request": { + "method": "VIEW", + "header": [], + "url": { + "raw": "" + } + }, + "response": [] + }, + { + "name": "delete challenge 1 by admin", + "event": [ + { + "listen": "test", + "script": { + "id": "7748e11e-0536-4d66-821e-2acb8b9ee4e4", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{admin_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID1}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID1}}" + ] + } + }, + "response": [] + }, + { + "name": "delete challenge 2 by copilot", + "event": [ + { + "listen": "test", + "script": { + "id": "21ce91c2-727e-4942-94ce-f7b9ad0e1922", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{copilot1_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge by different copilot 403", + "event": [ + { + "listen": "test", + "script": { + "id": "b27f9844-172a-45ab-8d78-d8e648162de7", + "exec": [ + "pm.test(\"Status code is 403\", function () {", + " pm.response.to.have.status(403);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{copilot2_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge by user 403", + "event": [ + { + "listen": "test", + "script": { + "id": "1d6af907-bdc2-437f-9883-3040c1e12e79", + "exec": [ + "pm.test(\"Status code is 403\", function () {", + " pm.response.to.have.status(403);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{user_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge without token 401", + "event": [ + { + "listen": "test", + "script": { + "id": "df9d716a-9a0f-4764-9154-2bd57532f0ea", + "exec": [ + "pm.test(\"Status code is 401\", function () {", + " pm.response.to.have.status(401);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge with invalid token 401", + "event": [ + { + "listen": "test", + "script": { + "id": "db2448ac-deda-4c6c-a47b-6c7af95ddf8b", + "exec": [ + "pm.test(\"Status code is 401\", function () {", + " pm.response.to.have.status(401);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge with expire token 401", + "event": [ + { + "listen": "test", + "script": { + "id": "2edcc5f2-8a0f-4791-93b2-da3a897daf76", + "exec": [ + "pm.test(\"Status code is 401\", function () {", + " pm.response.to.have.status(401);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{expire_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge not found 404", + "event": [ + { + "listen": "test", + "script": { + "id": "9e8ed6d8-7a40-4ced-8176-fed5a8d8b095", + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{admin_token}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/:challengeId", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + ":challengeId" + ], + "variable": [ + { + "key": "challengeId", + "value": "19d20d64-e84e-452f-abd5-018e6abc68ab" + } + ] + } + }, + "response": [] + }, + { + "name": "delete challenge using m2m token", + "event": [ + { + "listen": "test", + "script": { + "id": "c46553ad-8bb4-46b2-84ec-ba7a029b74ee", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{m2m_challenges_delete}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + }, + { + "name": "failure delete challenge using forbidden m2m token 403", + "event": [ + { + "listen": "test", + "script": { + "id": "3eeabff3-ceee-47f5-a23c-b2ff1322a2e8", + "exec": [ + "pm.test(\"Status code is 403\", function () {", + " pm.response.to.have.status(403);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/json" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{m2m_challenge_attachments_read}}" + } + ], + "url": { + "raw": "{{URL}}/challenges/{{CHALLENGE_ID2}}", + "host": [ + "{{URL}}" + ], + "path": [ + "challenges", + "{{CHALLENGE_ID2}}" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {}, + "_postman_isSubFolder": true } ], "protocolProfileBehavior": {} diff --git a/docs/topcoder-challenge-api.postman_environment.json b/docs/topcoder-challenge-api.postman_environment.json index 14c31335..98fe5b82 100644 --- a/docs/topcoder-challenge-api.postman_environment.json +++ b/docs/topcoder-challenge-api.postman_environment.json @@ -152,6 +152,11 @@ "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2NDA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6Y2hhbGxlbmdlcyBhbGw6Y2hhbGxlbmdlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.dN4uHhmPN33ljY2YSZmndxAnSXaowQaYENiPL5wjDvc", "enabled": true }, + { + "key": "m2m_challenges_delete", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2NDA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJkZWxldGU6Y2hhbGxlbmdlcyBhbGw6Y2hhbGxlbmdlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.NCZdRy3TErhxG5A2ic4ahC70xfihq2UA5y0Z7OyR7Go", + "enabled": true + }, { "key": "m2m_challenge_types_create", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2NDA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6Y2hhbGxlbmdlX3R5cGVzIGFsbDpjaGFsbGVuZ2VfdHlwZXMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.Njg1n3o0I8J9aa6y8xdiwfZFIGH2rJmW-7vKv3kXg5I", diff --git a/local/docker-compose.yml b/local/docker-compose.yml index e58c8a80..88b4db1c 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -3,8 +3,8 @@ services: dynamodb: image: tray/dynamodb-local ports: - - "7777:7777" - command: "-inMemory -port 7777" + - "8000:8000" + command: "-inMemory -port 8000" esearch: image: "docker.elastic.co/elasticsearch/elasticsearch:6.8.0" ports: @@ -24,6 +24,8 @@ services: build: context: ../ dockerfile: mock-api/Dockerfile + volumes: + - ../mock-api:/challenge-api/mock-api environment: DYNAMODB_URL: http://dynamodb:7777 IS_LOCAL_DB: "true" diff --git a/mock-api/app.js b/mock-api/app.js index 90fea518..ac97e1f4 100755 --- a/mock-api/app.js +++ b/mock-api/app.js @@ -14,8 +14,32 @@ app.set('port', config.PORT) app.use(cors()) +const groups = { + '33ba038e-48da-487b-96e8-8d3b99b6d181': { + id: '33ba038e-48da-487b-96e8-8d3b99b6d181', + name: 'group1', + description: 'desc1', + privateGroup: false, + selfRegister: true, + domain: 'domain1' + }, + '33ba038e-48da-487b-96e8-8d3b99b6d182': { + id: '33ba038e-48da-487b-96e8-8d3b99b6d182', + name: 'group2', + description: 'desc2', + privateGroup: true, + selfRegister: false, + domain: 'domain2' + } +} + // get challenge resources app.get('/v5/resources', (req, res) => { + winston.debug(`query: ${JSON.stringify(req.query, null, 2)}`) + if (Number(req.query.page) > 1) { + res.json([]) + return + } const challengeId = req.query.challengeId winston.info(`Get resources of challenge id ${challengeId}`) @@ -118,6 +142,17 @@ app.get('/v5/groups', (req, res) => { res.json(result) }) +// get group by id +app.get('/v5/groups/:groupId', (req, res) => { + winston.info(`Find group, groupId: ${req.params.groupId}`) + if (!groups[req.params.groupId]) { + res.status(404).send({ message: `group ${req.params.groupId} not found` }) + } + const result = groups[req.params.groupId] + winston.info(`Result: ${JSON.stringify(result, null, 4)}`) + res.json(result) +}) + // terms API app.get('/v5/terms/:termId', (req, res) => { const termId = req.params.termId diff --git a/src/common/helper.js b/src/common/helper.js index 03d6bfb7..6bae877f 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -763,6 +763,43 @@ async function validateChallengeTerms (terms = []) { return listOfTerms } +/** + * Calculate the sum of prizes. + * + * @param {Array} prizes the list of prize + * @returns {Number} the result prize + */ +function sumOfPrizes (prizes) { + let sum = 0 + if (!prizes.length) { + return sum + } + for (const prize of prizes) { + sum += prize.value + } + return sum +} + +/** + * Get group by id + * @param {String} groupId the group id + * @returns {Promise<Object>} the group + */ +async function getGroupById (groupId) { + const token = await getM2MToken() + try { + const result = await axios.get(`${config.GROUPS_API_URL}/${groupId}`, { + headers: { Authorization: `Bearer ${token}` } + }) + return result.data + } catch (err) { + if (err.response.status === HttpStatus.NOT_FOUND) { + return + } + throw err + } +} + module.exports = { wrapExpress, autoWrapExpress, @@ -798,5 +835,7 @@ module.exports = { getCompleteUserGroupTreeIds, expandWithParentGroups, getResourceRoles, - userHasFullAccess + userHasFullAccess, + sumOfPrizes, + getGroupById } diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 7f611d8a..61586671 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -60,10 +60,22 @@ async function partiallyUpdateChallenge (req, res) { res.send(result) } +/** + * Delete challenge + * @param {Object} req the request + * @param {Object} res the response + */ +async function deleteChallenge (req, res) { + logger.debug(`deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}`) + const result = await service.deleteChallenge(req.authUser, req.params.challengeId) + res.send(result) +} + module.exports = { searchChallenges, createChallenge, getChallenge, fullyUpdateChallenge, - partiallyUpdateChallenge + partiallyUpdateChallenge, + deleteChallenge } diff --git a/src/models/Challenge.js b/src/models/Challenge.js index 72be37ed..8ddbf407 100644 --- a/src/models/Challenge.js +++ b/src/models/Challenge.js @@ -112,6 +112,9 @@ const schema = new Schema({ type: [Object], required: false }, + overview: { + type: Object + }, created: { type: Date, required: true diff --git a/src/routes.js b/src/routes.js index f7ed7d27..70ff73da 100644 --- a/src/routes.js +++ b/src/routes.js @@ -52,6 +52,13 @@ module.exports = { auth: 'jwt', access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager], scopes: [UPDATE, ALL] + }, + delete: { + controller: 'ChallengeController', + method: 'deleteChallenge', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager], + scopes: [DELETE, ALL] } }, '/challenge-types': { diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 1ce204e3..9a5c3ce4 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -21,6 +21,20 @@ const ChallengeTimelineTemplateService = require('./ChallengeTimelineTemplateSer const esClient = helper.getESClient() +/** + * Check if user can perform modification/deletion to a challenge + * + * @param {Object} user the JwT user object + * @param {Object} challenge the challenge object + * @returns {undefined} + */ +async function ensureAccessibleForChallenge (user, challenge) { + const userHasFullAccess = await helper.userHasFullAccess(challenge.id, user.userId) + if (!user.isMachine && !helper.hasAdminRole(user) && challenge.createdBy.toLowerCase() !== user.handle.toLowerCase() && !userHasFullAccess) { + throw new errors.ForbiddenError(`Only M2M, admin, challenge's copilot or users with full access can perform modification.`) + } +} + /** * Filter challenges by groups access * @param {Object} currentUser the user who perform operation @@ -342,9 +356,27 @@ async function searchChallenges (currentUser, criteria) { if (!_.isUndefined(criteria.group) || !_.isUndefined(criteria.groups)) { // check group access if (_.isUndefined(currentUser)) { - throw new errors.BadRequestError('Authentication is required to filter challenges based on groups') - } - if (!currentUser.isMachine && !helper.hasAdminRole(currentUser)) { + if (criteria.group) { + const group = await helper.getGroupById(criteria.group) + if (group && !group.privateGroup) { + groupsToFilter.push(criteria.group) + } + } + if (criteria.groups && criteria.groups.length > 0) { + const promises = [] + _.each(criteria.groups, (g) => { + promises.push( + (async () => { + const group = await helper.getGroupById(g) + if (group && !group.privateGroup) { + groupsToFilter.push(g) + } + })() + ) + }) + await Promise.all(promises) + } + } else if (!currentUser.isMachine && !helper.hasAdminRole(currentUser)) { if (accessibleGroups.includes(criteria.group)) { groupsToFilter.push(criteria.group) } @@ -848,6 +880,16 @@ async function createChallenge (currentUser, challenge, userToken) { if (challenge.phases && challenge.phases.length > 0) { challenge.endDate = helper.calculateChallengeEndDate(challenge) } + + // auto-populate totalPrizes + if (challenge.prizeSets) { + const prizeSetsGroup = _.groupBy(challenge.prizeSets, 'type') + if (prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes]) { + const totalPrizes = helper.sumOfPrizes(prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes][0].prizes) + _.assign(challenge, { overview: { totalPrizes } }) + } + } + const ret = await helper.create('Challenge', _.assign({ id: uuid(), created: moment().utc(), @@ -1193,10 +1235,7 @@ async function update (currentUser, challengeId, data, userToken, isFull) { newAttachments = await helper.getByIds('Attachment', data.attachmentIds || []) } - const userHasFullAccess = await helper.userHasFullAccess(challengeId, currentUser.userId) - if (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase() && !userHasFullAccess) { - throw new errors.ForbiddenError(`Only M2M, admin, challenge's copilot or users with full access can perform modification.`) - } + await ensureAccessibleForChallenge(currentUser, challenge) // Only M2M can update url and options of discussions if (data.discussions && data.discussions.length > 0) { @@ -1266,6 +1305,18 @@ async function update (currentUser, challengeId, data, userToken, isFull) { throw new errors.BadRequestError(`Cannot change the timelineTemplateId for challenges with status: ${finalStatus}`) } + if (data.prizeSets) { + const prizeSetsGroup = _.groupBy(data.prizeSets, 'type') + if (!prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes] && _.get(challenge, 'overview.totalPrizes')) { + // remove the totalPrizes if challenge prizes are empty + challenge.overview = _.omit(challenge.overview, ['totalPrizes']) + } else { + const totalPrizes = helper.sumOfPrizes(prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes][0].prizes) + logger.debug(`re-calculate total prizes, current value is ${totalPrizes.value}`) + _.assign(challenge, { overview: { totalPrizes } }) + } + } + if (data.phases || data.startDate) { if (data.phases && data.phases.length > 0) { for (let i = 0; i < challenge.phases.length; i += 1) { @@ -1555,6 +1606,17 @@ async function update (currentUser, challengeId, data, userToken, isFull) { if (challenge.phases && challenge.phases.length > 0) { challenge.currentPhase = challenge.phases.slice().reverse().find(phase => phase.isOpen) challenge.endDate = helper.calculateChallengeEndDate(challenge) + const registrationPhase = _.find(challenge.phases, p => p.name === 'Registration') + const submissionPhase = _.find(challenge.phases, p => p.name === 'Submission') + challenge.currentPhaseNames = _.map(_.filter(challenge.phases, p => p.isOpen === true), 'name') + if (registrationPhase) { + challenge.registrationStartDate = registrationPhase.actualStartDate || registrationPhase.scheduledStartDate + challenge.registrationEndDate = registrationPhase.actualEndDate || registrationPhase.scheduledEndDate + } + if (submissionPhase) { + challenge.submissionStartDate = submissionPhase.actualStartDate || submissionPhase.scheduledStartDate + challenge.submissionEndDate = submissionPhase.actualEndDate || submissionPhase.scheduledEndDate + } } // Update ES await esClient.update({ @@ -1722,7 +1784,8 @@ fullyUpdateChallenge.schema = { terms: Joi.array().items(Joi.object().keys({ id: Joi.id(), roleId: Joi.id() - }).unknown(true)).optional().allow([]) + }).unknown(true)).optional().allow([]), + overview: Joi.any().forbidden() }).unknown(true).required(), userToken: Joi.any() } @@ -1807,17 +1870,52 @@ partiallyUpdateChallenge.schema = { handle: Joi.string().required(), placement: Joi.number().integer().positive().required() }).unknown(true)).min(1), - terms: Joi.array().items(Joi.id().optional()).optional().allow([]) + terms: Joi.array().items(Joi.id().optional()).optional().allow([]), + overview: Joi.any().forbidden() }).unknown(true).required(), userToken: Joi.any() } +/** + * Delete challenge. + * @param {Object} currentUser the user who perform operation + * @param {String} challengeId the challenge id + * @returns {Object} the deleted challenge + */ +async function deleteChallenge (currentUser, challengeId) { + const challenge = await helper.getById('Challenge', challengeId) + if (challenge.status !== constants.challengeStatuses.New) { + throw new errors.BadRequestError(`Challenge with status other than "${constants.challengeStatuses.New}" cannot be removed`) + } + // check groups authorization + await ensureAccessibleByGroupsAccess(currentUser, challenge) + // check if user are allowed to delete the challenge + await ensureAccessibleForChallenge(currentUser, challenge) + // delete DB record + await models.Challenge.delete(challenge) + // delete ES document + await esClient.delete({ + index: config.get('ES.ES_INDEX'), + type: config.get('ES.ES_TYPE'), + refresh: config.get('ES.ES_REFRESH'), + id: challengeId + }) + await helper.postBusEvent(constants.Topics.ChallengeDeleted, { id: challengeId }) + return challenge +} + +deleteChallenge.schema = { + currentUser: Joi.any(), + challengeId: Joi.id() +} + module.exports = { searchChallenges, createChallenge, getChallenge, fullyUpdateChallenge, - partiallyUpdateChallenge + partiallyUpdateChallenge, + deleteChallenge } -// logger.buildService(module.exports) +logger.buildService(module.exports)