Skip to content

fix: challenge update #578

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 27 commits into from
Mar 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9b68f91
fix: merge phase-helper updates
rakibansary Mar 23, 2023
e062c36
refactor: challenge update
rakibansary Mar 23, 2023
07c66a0
Merge branch 'refactor/domain-challenge-dev' into refactor/challenge-…
rakibansary Mar 23, 2023
1fd1ea4
refactor: only update whats necessary in challenge:update
rakibansary Mar 23, 2023
396db36
ci: deploy to dev
rakibansary Mar 23, 2023
704f88d
fix: missing variables
rakibansary Mar 23, 2023
c44d4d4
fix: enable indexing and resource creation
rakibansary Mar 23, 2023
c1945c3
wip
rakibansary Mar 23, 2023
5e4b07f
feat: remove attributes that match exactly with saved challenge
rakibansary Mar 24, 2023
e70465e
refactor: challenge update
rakibansary Mar 24, 2023
8782f0e
fix: allow memberId to be a number
rakibansary Mar 24, 2023
e3d6dcd
fix: getM2MToken reference
eisbilir Mar 24, 2023
fa398ce
Merge pull request #572 from topcoder-platform/fix/getm2mtoken
rakibansary Mar 24, 2023
669c1b1
fix: winner can not be set for non tasks
rakibansary Mar 24, 2023
243b6e8
fix: remove hardcoded debug code
rakibansary Mar 24, 2023
06578d3
fix: unsetting winners
rakibansary Mar 24, 2023
68a7dfd
fix: phase update object
eisbilir Mar 24, 2023
80a4689
fix: allow private description to be empty
eisbilir Mar 24, 2023
99c8645
Merge pull request #573 from topcoder-platform/fix/phase-update
rakibansary Mar 24, 2023
f0f4dd7
fix: phases
eisbilir Mar 24, 2023
8375f2f
Merge pull request #574 from topcoder-platform/fix/phase-update
eisbilir Mar 24, 2023
3104444
chore: handle phase update
rakibansary Mar 24, 2023
b60daa8
validation updates
ThomasKranitsas Mar 25, 2023
28d1646
unset legacy.directProjectId as we do not allowe this to change
ThomasKranitsas Mar 25, 2023
33247ca
fix: bump up versions
rakibansary Mar 25, 2023
8ce4e02
fix: prize amount in cents
rakibansary Mar 26, 2023
1c4dd3a
Merge pull request #576 from topcoder-platform/fix/update-validation
rakibansary Mar 26, 2023
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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ workflows:
branches:
only:
- refactor/domain-challenge-dev
- refactor/challenge-update

- "build-qa":
context: org-global
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@
"dependencies": {
"@grpc/grpc-js": "^1.8.12",
"@opensearch-project/opensearch": "^2.2.0",
"@topcoder-framework/domain-challenge": "^0.7.3",
"@topcoder-framework/lib-common": "^0.7.3",
"@topcoder-framework/domain-challenge": "^0.10.13",
"@topcoder-framework/lib-common": "^0.10.13",
"aws-sdk": "^2.1145.0",
"axios": "^0.19.0",
"axios-retry": "^3.4.0",
"bluebird": "^3.5.1",
"body-parser": "^1.15.1",
"config": "^3.0.1",
"cors": "^2.7.1",
"deep-equal": "^2.2.0",
"dotenv": "^8.2.0",
"dynamoose": "^1.11.1",
"elasticsearch": "^16.7.3",
Expand Down
249 changes: 248 additions & 1 deletion src/common/challenge-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const constants = require("../../app-constants");
const axios = require("axios");
const { getM2MToken } = require("./m2m-helper");
const { hasAdminRole } = require("./role-helper");
const { ensureAcessibilityToModifiedGroups } = require("./group-helper");

class ChallengeHelper {
/**
Expand Down Expand Up @@ -45,7 +46,7 @@ class ChallengeHelper {
* @param {String} projectId the project id
* @param {String} currentUser the user
*/
async ensureProjectExist(projectId, currentUser) {
static async ensureProjectExist(projectId, currentUser) {
let token = await getM2MToken();
const url = `${config.PROJECTS_API_URL}/${projectId}`;
try {
Expand Down Expand Up @@ -98,6 +99,252 @@ class ChallengeHelper {
// check groups authorization
await helper.ensureAccessibleByGroupsAccess(currentUser, challenge);
}

async validateChallengeUpdateRequest(currentUser, challenge, data) {
if (process.env.LOCAL != "true") {
await helper.ensureUserCanModifyChallenge(currentUser, challenge);
}

helper.ensureNoDuplicateOrNullElements(data.tags, "tags");
helper.ensureNoDuplicateOrNullElements(data.groups, "groups");

if (data.projectId) {
await ChallengeHelper.ensureProjectExist(data.projectId, currentUser);
}

// check groups access to be updated group values
if (data.groups) {
await ensureAcessibilityToModifiedGroups(currentUser, data, challenge);
}

// Ensure descriptionFormat is either 'markdown' or 'html'
if (data.descriptionFormat && !_.includes(["markdown", "html"], data.descriptionFormat)) {
throw new errors.BadRequestError("The property 'descriptionFormat' must be either 'markdown' or 'html'");
}

// Ensure unchangeable fields are not changed
if (
_.get(challenge, "legacy.track") &&
_.get(data, "legacy.track") &&
_.get(challenge, "legacy.track") !== _.get(data, "legacy.track")
) {
throw new errors.ForbiddenError("Cannot change legacy.track");
}

if (
_.get(challenge, "trackId") &&
_.get(data, "trackId") &&
_.get(challenge, "trackId") !== _.get(data, "trackId")
) {
throw new errors.ForbiddenError("Cannot change trackId");
}

if (
_.get(challenge, "typeId") &&
_.get(data, "typeId") &&
_.get(challenge, "typeId") !== _.get(data, "typeId")
) {
throw new errors.ForbiddenError("Cannot change typeId");
}

if (
_.get(challenge, "legacy.pureV5Task") &&
_.get(data, "legacy.pureV5Task") &&
_.get(challenge, "legacy.pureV5Task") !== _.get(data, "legacy.pureV5Task")
) {
throw new errors.ForbiddenError("Cannot change legacy.pureV5Task");
}

if (
_.get(challenge, "legacy.pureV5") &&
_.get(data, "legacy.pureV5") &&
_.get(challenge, "legacy.pureV5") !== _.get(data, "legacy.pureV5")
) {
throw new errors.ForbiddenError("Cannot change legacy.pureV5");
}

if (
_.get(challenge, "legacy.selfService") &&
_.get(data, "legacy.selfService") &&
_.get(challenge, "legacy.selfService") !== _.get(data, "legacy.selfService")
) {
throw new errors.ForbiddenError("Cannot change legacy.selfService");
}

if (
(challenge.status === constants.challengeStatuses.Completed ||
challenge.status === constants.challengeStatuses.Cancelled) &&
data.status &&
data.status !== challenge.status &&
data.status !== constants.challengeStatuses.CancelledClientRequest
) {
throw new errors.BadRequestError(
`Cannot change ${challenge.status} challenge status to ${data.status} status`
);
}

if (
data.winners &&
data.winners.length > 0 &&
challenge.status !== constants.challengeStatuses.Completed &&
data.status !== constants.challengeStatuses.Completed
) {
throw new errors.BadRequestError(
`Cannot set winners for challenge with non-completed ${challenge.status} status`
);
}
}

sanitizeRepeatedFieldsInUpdateRequest(data) {
if (data.winners != null) {
data.winnerUpdate = {
winners: data.winners,
};
delete data.winners;
}

if (data.discussions != null) {
data.discussionUpdate = {
discussions: data.discussions,
};
delete data.discussions;
}

if (data.metadata != null) {
data.metadataUpdate = {
metadata: data.metadata,
};
delete data.metadata;
}

if (data.phases != null) {
data.phaseUpdate = {
phases: data.phases,
};
delete data.phases;
}

if (data.events != null) {
data.eventUpdate = {
events: data.events,
};
delete data.events;
}

if (data.terms != null) {
data.termUpdate = {
terms: data.terms,
};
delete data.terms;
}

if (data.prizeSets != null) {
data.prizeSetUpdate = {
prizeSets: data.prizeSets,
};
delete data.prizeSets;
}

if (data.tags != null) {
data.tagUpdate = {
tags: data.tags,
};
delete data.tags;
}

if (data.attachments != null) {
data.attachmentUpdate = {
attachments: data.attachments,
};
delete data.attachments;
}

if (data.groups != null) {
data.groupUpdate = {
groups: data.groups,
};
delete data.groups;
}

return data;
}

enrichChallengeForResponse(challenge, track, type) {
if (challenge.phases && challenge.phases.length > 0) {
const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration");
const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission");

challenge.currentPhase = challenge.phases
.slice()
.reverse()
.find((phase) => phase.isOpen);

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;
}
}

challenge.created = new Date(challenge.created).toISOString();
challenge.updated = new Date(challenge.updated).toISOString();
challenge.startDate = new Date(challenge.startDate).toISOString();
challenge.endDate = new Date(challenge.endDate).toISOString();

if (track) {
challenge.track = track.name;
}

if (type) {
challenge.type = type.name;
}

challenge.metadata = challenge.metadata.map((m) => {
try {
m.value = JSON.stringify(JSON.parse(m.value)); // when we update how we index data, make this a JSON field
} catch (err) {
// do nothing
}
return m;
});
}

convertPrizeSetValuesToCents(prizeSets) {
prizeSets.forEach((prizeSet) => {
prizeSet.prizes.forEach((prize) => {
prize.amountInCents = prize.value * 100;
delete prize.value;
});
});
}

convertPrizeSetValuesToDollars(prizeSets, overview) {
prizeSets.forEach((prizeSet) => {
prizeSet.prizes.forEach((prize) => {
if (prize.amountInCents != null) {
prize.value = prize.amountInCents / 100;
delete prize.amountInCents;
}
});
});
if (overview && overview.totalPrizesInCents) {
overview.totalPrizes = overview.totalPrizesInCents / 100;
delete overview.totalPrizesInCents;
}
}
}

module.exports = new ChallengeHelper();
36 changes: 36 additions & 0 deletions src/common/group-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const _ = require("lodash");
const errors = require("./errors");
const helper = require("./helper");

const { hasAdminRole } = require("./role-helper");

class GroupHelper {
/**
* Ensure the user can access the groups being updated to
* @param {Object} currentUser the user who perform operation
* @param {Object} data the challenge data to be updated
* @param {String} challenge the original challenge data
*/
async ensureAcessibilityToModifiedGroups(currentUser, data, challenge) {
const needToCheckForGroupAccess = !currentUser
? true
: !currentUser.isMachine && !hasAdminRole(currentUser);
if (!needToCheckForGroupAccess) {
return;
}
const userGroups = await helper.getUserGroups(currentUser.userId);
const userGroupsIds = _.map(userGroups, (group) => group.id);
const updatedGroups = _.difference(
_.union(challenge.groups, data.groups),
_.intersection(challenge.groups, data.groups)
);
const filtered = updatedGroups.filter((g) => !userGroupsIds.includes(g));
if (filtered.length > 0) {
throw new errors.ForbiddenError(
"ensureAcessibilityToModifiedGroups :: You don't have access to this group!"
);
}
}
}

module.exports = new GroupHelper();
9 changes: 6 additions & 3 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,16 +450,19 @@ axiosRetry(axios, {
* @param {String} token The token
* @returns
*/
async function createSelfServiceProject(name, description, type, token) {
async function createSelfServiceProject(name, description, type) {
const projectObj = {
name,
description,
type,
};

const token = await m2mHelper.getM2MToken();
const url = `${config.PROJECTS_API_URL}`;
const res = await axios.post(url, projectObj, {
headers: { Authorization: `Bearer ${token}` },
});

return _.get(res, "data.id");
}

Expand Down Expand Up @@ -942,7 +945,7 @@ async function listChallengesByMember(memberId) {
* @returns {Promise<Array>} an array of resources.
*/
async function listResourcesByMemberAndChallenge(memberId, challengeId) {
const token = await getM2MToken();
const token = await m2mHelper.getM2MToken();
let response = {};
try {
response = await axios.get(config.RESOURCES_API_URL, {
Expand Down Expand Up @@ -1123,7 +1126,7 @@ async function ensureUserCanViewChallenge(currentUser, challenge) {
*
* @param {Object} currentUser the user who perform operation
* @param {Object} challenge the challenge to check
* @returns {undefined}
* @returns {Promise}
*/
async function ensureUserCanModifyChallenge(currentUser, challenge) {
// check groups authorization
Expand Down
8 changes: 5 additions & 3 deletions src/common/m2m-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ const config = require("config");
const m2mAuth = require("tc-core-library-js").auth.m2m;

class M2MHelper {
static m2m = null;

constructor() {
this.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"]));
M2MHelper.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"]));
}
/**
* Get M2M token.
* @returns {Promise<String>} the M2M token
*/
async getM2MToken() {
return this.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET);
getM2MToken() {
return M2MHelper.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET);
}
}

Expand Down
Loading