Skip to content

Commit 30a34e2

Browse files
committed
Merge branch 'security' of github.com:topcoder-platform/challenge-api-v6 into security
2 parents 81b85c5 + fcf4ede commit 30a34e2

File tree

9 files changed

+403
-91
lines changed

9 files changed

+403
-91
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `isAIReviewer` on the `DefaultChallengeReviewer` table. All the data in the column will be lost.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "DefaultChallengeReviewer" DROP COLUMN "isAIReviewer",
9+
ADD COLUMN "aiWorkflowId" VARCHAR(14),
10+
ALTER COLUMN "scorecardId" DROP NOT NULL;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Improve search responsiveness for group-constrained queries
2+
CREATE INDEX IF NOT EXISTS "challenge_groups_gin_idx"
3+
ON "challenges"."Challenge"
4+
USING GIN ("groups");

prisma/schema.prisma

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ model Challenge {
148148
@@index([updatedAt])
149149
@@index([typeId])
150150
@@index([trackId])
151+
@@index([groups], type: Gin, map: "challenge_groups_gin_idx")
151152
@@index([submissionStartDate])
152153
@@index([submissionEndDate])
153154
@@index([registrationStartDate])
@@ -648,7 +649,7 @@ model DefaultChallengeReviewer {
648649
trackId String
649650
timelineTemplateId String?
650651
// Reviewer configuration (mirrors ChallengeReviewer)
651-
scorecardId String
652+
scorecardId String?
652653
isMemberReview Boolean
653654
memberReviewerCount Int?
654655
phaseName String
@@ -658,7 +659,7 @@ model DefaultChallengeReviewer {
658659
baseCoefficient Float?
659660
incrementalCoefficient Float?
660661
opportunityType ReviewOpportunityTypeEnum?
661-
isAIReviewer Boolean?
662+
aiWorkflowId String? @db.VarChar(14)
662663
shouldOpenOpportunity Boolean @default(true)
663664
664665
// Relations
@@ -723,4 +724,4 @@ model TimelineTemplatePhase {
723724
724725
@@index([timelineTemplateId])
725726
@@index([timelineTemplateId, phaseId])
726-
}
727+
}

src/common/challenge-helper.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const { hasAdminRole } = require("./role-helper");
1212
const { ensureAcessibilityToModifiedGroups } = require("./group-helper");
1313
const { ChallengeStatusEnum } = require("@prisma/client");
1414

15+
const SUBMISSION_PHASE_PRIORITY = ["Topcoder Submission", "Submission"];
16+
1517
class ChallengeHelper {
1618
/**
1719
* @param {Object} challenge the challenge object
@@ -356,7 +358,9 @@ class ChallengeHelper {
356358
enrichChallengeForResponse(challenge, track, type, options = {}) {
357359
if (challenge.phases && challenge.phases.length > 0) {
358360
const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration");
359-
const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission");
361+
const submissionPhase =
362+
_.find(challenge.phases, (p) => p.name === SUBMISSION_PHASE_PRIORITY[0]) ||
363+
_.find(challenge.phases, (p) => p.name === SUBMISSION_PHASE_PRIORITY[1]);
360364

361365
// select last started open phase as current phase
362366
_.forEach(challenge.phases, (p) => {

src/common/prisma-helper.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const Decimal = require("decimal.js");
33
const constants = require("../../app-constants");
44
const { PrizeSetTypeEnum } = require("@prisma/client");
55
const { dedupeChallengeTerms } = require("./helper");
6+
7+
const SUBMISSION_PHASE_PRIORITY = ["Topcoder Submission", "Submission"];
68
/**
79
* Convert phases data to prisma model.
810
*
@@ -11,6 +13,7 @@ const { dedupeChallengeTerms } = require("./helper");
1113
* @param {Object} auditFields createdBy and updatedBy
1214
*/
1315
function convertChallengePhaseSchema(challenge, result, auditFields) {
16+
const phases = _.isArray(challenge.phases) ? challenge.phases : [];
1417
// keep phase data
1518
const phaseFields = [
1619
"name",
@@ -25,24 +28,29 @@ function convertChallengePhaseSchema(challenge, result, auditFields) {
2528
"challengeSource",
2629
];
2730
// current phase names
28-
result.currentPhaseNames = _.map(
29-
_.filter(challenge.phases, (p) => p.isOpen === true),
30-
"name"
31-
);
32-
// get registration date and submission date
33-
_.forEach(challenge.phases, (p) => {
34-
if (p.name === "Registration") {
35-
result.registrationStartDate = p.actualStartDate || p.scheduledStartDate;
36-
result.registrationEndDate = p.actualEndDate || p.scheduledEndDate;
37-
} else if (p.name === "Submission") {
38-
result.submissionStartDate = p.actualStartDate || p.scheduledStartDate;
39-
result.submissionEndDate = p.actualEndDate || p.scheduledEndDate;
40-
}
41-
});
31+
result.currentPhaseNames = _.map(_.filter(phases, (p) => p.isOpen === true), "name");
32+
33+
const registrationPhase = _.find(phases, (p) => p.name === "Registration");
34+
const submissionPhase =
35+
_.find(phases, (p) => p.name === SUBMISSION_PHASE_PRIORITY[0]) ||
36+
_.find(phases, (p) => p.name === SUBMISSION_PHASE_PRIORITY[1]);
37+
38+
if (registrationPhase) {
39+
result.registrationStartDate =
40+
registrationPhase.actualStartDate || registrationPhase.scheduledStartDate;
41+
result.registrationEndDate =
42+
registrationPhase.actualEndDate || registrationPhase.scheduledEndDate;
43+
}
44+
if (submissionPhase) {
45+
result.submissionStartDate =
46+
submissionPhase.actualStartDate || submissionPhase.scheduledStartDate;
47+
result.submissionEndDate =
48+
submissionPhase.actualEndDate || submissionPhase.scheduledEndDate;
49+
}
4250
// set phases array data
43-
if (!_.isEmpty(challenge.phases)) {
51+
if (!_.isEmpty(phases)) {
4452
result.phases = {
45-
create: _.map(challenge.phases, (p) => {
53+
create: _.map(phases, (p) => {
4654
const phaseData = {
4755
phase: { connect: { id: p.phaseId } },
4856
..._.pick(p, phaseFields),

src/services/ChallengePhaseService.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const PHASE_RESOURCE_ROLE_REQUIREMENTS = Object.freeze({
3131
review: "Reviewer",
3232
"checkpoint review": "Checkpoint Reviewer",
3333
});
34+
const SUBMISSION_PHASE_NAME_SET = new Set(["submission", "topgear submission"]);
35+
const REGISTRATION_PHASE_NAME = "registration";
36+
37+
const normalizePhaseName = (name) => String(name || "").trim().toLowerCase();
3438

3539
async function hasPendingScorecardsForPhase(challengePhaseId) {
3640
if (!config.REVIEW_DB_URL) {
@@ -561,7 +565,14 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data)
561565
return reopenedPhaseIdentifiers.has(String(phase.predecessor));
562566
});
563567

564-
if (dependentOpenPhases.length === 0) {
568+
const normalizedPhaseName = normalizePhaseName(phaseName);
569+
const hasSubmissionVariantOpen = openPhases.some((phase) =>
570+
SUBMISSION_PHASE_NAME_SET.has(normalizePhaseName(phase?.name))
571+
);
572+
const allowRegistrationReopenWithoutExplicitDependency =
573+
normalizedPhaseName === REGISTRATION_PHASE_NAME && hasSubmissionVariantOpen;
574+
575+
if (dependentOpenPhases.length === 0 && !allowRegistrationReopenWithoutExplicitDependency) {
565576
throw new errors.ForbiddenError(
566577
`Cannot reopen ${phaseName} because no currently open phase depends on it`
567578
);

0 commit comments

Comments
 (0)