Skip to content

[PROD RELEASE]#72

Open
kkartunov wants to merge 28 commits intomasterfrom
develop
Open

[PROD RELEASE]#72
kkartunov wants to merge 28 commits intomasterfrom
develop

Conversation

@kkartunov
Copy link
Contributor

@jmgasper please fill in.

jmgasper and others added 28 commits February 6, 2026 09:40
What was broken\nMarathon tournament fun challenges had no explicit API flag, so downstream apps could not reliably distinguish leaderboard-only challenges from prize-paying challenges.\n\nRoot cause\nThe Challenge schema and API payload validation/sanitization did not include a dedicated fun challenge boolean.\n\nWhat was changed\nAdded funChallenge boolean (default false) to Prisma schemas and migration, wired it through create/update validation, Prisma mapping, sanitize output, and Swagger docs.\n\nAny added/updated tests\nUpdated ChallengeService unit test data/assertions to cover funChallenge persistence in create and update flows.
What was broken:\n- POST /v6/challenge-tracks rejected payloads that used legacy track values (DEVELOP and QA), returning 400 responses instead of creating/updating tracks.\n\nRoot cause:\n- The ChallengeTrack enum was standardized to DEVELOPMENT/QUALITY_ASSURANCE, but request validation and persistence paths did not normalize legacy values still used by clients.\n\nWhat was changed:\n- Added legacy alias normalization in ChallengeTrackService: DEVELOP -> DEVELOPMENT and QA -> QUALITY_ASSURANCE.\n- Applied normalization for search, create, full update, and partial update flows.\n- Updated Joi validation to accept both canonical enum values and legacy aliases (uppercased input).\n\nAny added/updated tests:\n- Added test/unit/ChallengeTrackService.test.js with focused unit tests for create/search/partial update alias behavior using stubbed Prisma methods.
PM- 3351 Send notifications to resources on manual phase change
callback(null, '*')
}
},
origin: "*",

Check warning

Code scanning / CodeQL

Permissive CORS configuration Medium

CORS Origin allows broad access due to
permissive or user controlled value
.

Copilot Autofix

AI about 22 hours ago

In general, the problem should be fixed by replacing the permissive origin: "*" configuration with either (a) a restrictive, explicit whitelist of allowed origins, or (b) origin: false where CORS is not needed. For APIs that genuinely need to be called from browsers, the typical approach is to maintain a list of trusted front‑end origins (e.g., via configuration) and only return CORS headers when the request’s Origin header matches that list.

For this specific code, the best fix without changing existing functionality too much is to move from origin: "*" to a whitelist‑based policy, driven by configuration values that can be adjusted per environment. We can use the cors package’s ability to accept a function for origin, which lets us check the incoming Origin header against an allowed list and only enable CORS for those origins. A natural place to get that list is config, which is already imported. We’ll treat config.CORS_ALLOWED_ORIGINS (if present) as an array of allowed origins, and otherwise fall back to a safe default (e.g., no origins allowed) instead of "*". This preserves flexibility while eliminating the unconditional * response. Concretely:

  • Add a small helper just before the app.use(cors(...)) call to compute CORS_ALLOWED_ORIGINS from config (e.g., an array or a comma‑separated string).
  • Replace origin: "*" with an origin callback that:
    • Allows requests with no Origin (non‑browser or same‑origin requests) by invoking the callback with null.
    • If the origin is in the allowed list, invokes the callback with null (allow).
    • Otherwise, invokes the callback with an error or false (deny CORS for that origin).

All changes will be confined to app.js around the CORS configuration, without touching other middleware or routes.


Suggested changeset 1
app.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app.js b/app.js
--- a/app.js
+++ b/app.js
@@ -68,9 +68,34 @@
   swaggerUi.setup(challengeAPIWithAuthDoc, { explorer: true })
 );
 
+// Configure CORS with an explicit whitelist of allowed origins from config
+const allowedCorsOrigins = (() => {
+  const fromConfig = config.CORS_ALLOWED_ORIGINS;
+  if (Array.isArray(fromConfig)) {
+    return fromConfig;
+  }
+  if (typeof fromConfig === "string" && fromConfig.trim().length > 0) {
+    return fromConfig
+      .split(",")
+      .map((o) => o.trim())
+      .filter((o) => o.length > 0);
+  }
+  return [];
+})();
+
 app.use(
   cors({
-    origin: "*",
+    origin: (origin, callback) => {
+      // Allow requests with no origin (e.g., curl, server-to-server, same-origin)
+      if (!origin) {
+        return callback(null, true);
+      }
+      if (allowedCorsOrigins.includes(origin)) {
+        return callback(null, true);
+      }
+      // Origin not allowed by CORS
+      return callback(null, false);
+    },
     exposedHeaders: [
       "X-Prev-Page",
       "X-Next-Page",
EOF
@@ -68,9 +68,34 @@
swaggerUi.setup(challengeAPIWithAuthDoc, { explorer: true })
);

// Configure CORS with an explicit whitelist of allowed origins from config
const allowedCorsOrigins = (() => {
const fromConfig = config.CORS_ALLOWED_ORIGINS;
if (Array.isArray(fromConfig)) {
return fromConfig;
}
if (typeof fromConfig === "string" && fromConfig.trim().length > 0) {
return fromConfig
.split(",")
.map((o) => o.trim())
.filter((o) => o.length > 0);
}
return [];
})();

app.use(
cors({
origin: "*",
origin: (origin, callback) => {
// Allow requests with no origin (e.g., curl, server-to-server, same-origin)
if (!origin) {
return callback(null, true);
}
if (allowedCorsOrigins.includes(origin)) {
return callback(null, true);
}
// Origin not allowed by CORS
return callback(null, false);
},
exposedHeaders: [
"X-Prev-Page",
"X-Next-Page",
Copilot is powered by AI and may make mistakes. Always verify output.
@kkartunov kkartunov requested a review from jmgasper February 26, 2026 08:30

const PhaseChangeNotificationSettings = {
PHASE_CHANGE: {
sendgridTemplateId: config.PHASE_CHANGE_SENDGRID_TEMPLATE_ID,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Ensure that config.PHASE_CHANGE_SENDGRID_TEMPLATE_ID is always defined and valid. If this value can be undefined or incorrect, it may lead to runtime errors when sending notifications.

callback(null, '*')
}
},
origin: "*",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Changing the CORS origin to "*" allows any domain to access the API, which can be a security risk. Consider restricting this to specific domains or using a more dynamic approach to determine allowed origins.

RESOURCES_DB_SCHEMA: process.env.RESOURCES_DB_SCHEMA || "resources",
REVIEW_DB_SCHEMA: process.env.REVIEW_DB_SCHEMA || "reviews",
CHALLENGE_SERVICE_PRISMA_TIMEOUT: process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT ? parseInt(process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT, 10) : 10000,
CHALLENGE_URL: process.env.CHALLENGE_URL || 'https://www.topcoder-dev.com/challenges' ,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The default value for CHALLENGE_URL is hardcoded to https://www.topcoder-dev.com/challenges. Ensure this is the intended production URL and not a development or staging URL.

REVIEW_DB_SCHEMA: process.env.REVIEW_DB_SCHEMA || "reviews",
CHALLENGE_SERVICE_PRISMA_TIMEOUT: process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT ? parseInt(process.env.CHALLENGE_SERVICE_PRISMA_TIMEOUT, 10) : 10000,
CHALLENGE_URL: process.env.CHALLENGE_URL || 'https://www.topcoder-dev.com/challenges' ,
PHASE_CHANGE_SENDGRID_TEMPLATE_ID: process.env.PHASE_CHANGE_SENDGRID_TEMPLATE_ID || "",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The PHASE_CHANGE_SENDGRID_TEMPLATE_ID is defaulting to an empty string. If this is required for production, consider adding a validation check to ensure it is set correctly.

* @returns {Promise<Object>} the group
*/
async function getGroupById(groupId) {
const normalizedGroupId = _.toString(groupId || "").trim();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
Consider using String(groupId).trim() instead of _.toString(groupId || '').trim() for better performance and to avoid unnecessary dependency on lodash.

try {
const result = await axios.get(`${config.GROUPS_API_URL}/${groupId}`, {
headers: { Authorization: `Bearer ${token}` },
const result = await axios.get(`${config.GROUPS_API_URL}/${encodeURIComponent(normalizedGroupId)}`, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Ensure that encodeURIComponent is used consistently for all dynamic URL segments to prevent potential injection attacks.

);
return;
}
const safeRecipients = Array.isArray(recipients) ? recipients.filter(Boolean) : [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Filtering recipients with recipients.filter(Boolean) is a good practice to avoid sending emails to invalid addresses. Ensure that this logic is applied consistently across all notification functions.

version: 'v3',
},
);
} catch (e) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ reliability]
Consider adding a retry mechanism for postBusEvent to handle transient network errors more gracefully.

await postChallengeUpdatedNotification(challengeId);

// send notification logic
try {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The try-catch block is catching all exceptions and only logging them at the debug level. Consider logging at a higher level (e.g., error) or rethrowing the exception after logging to ensure that critical failures are not silently ignored.

: (result.actualStartDate || new Date().toISOString());

// fetch challenge name
const challenge = await prisma.challenge.findUnique({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The prisma.challenge.findUnique call does not handle the case where the challenge is not found. Consider adding error handling for this scenario to avoid potential null reference errors when accessing challenge.name.

const challengeName = challenge?.name;

// build recipients
const resources = await helper.getChallengeResources(challengeId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The helper.getChallengeResources function call assumes that it will return a non-null value. Consider adding a null check to handle cases where the function might return null or undefined to prevent runtime errors.

at,
});

await helper.sendPhaseChangeNotification(notificationType, recipients, payload);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The helper.sendPhaseChangeNotification function is called without handling potential exceptions that might occur during the notification sending process. Consider wrapping this call in a try-catch block to handle any errors gracefully.

QA: ChallengeTrackEnum.QUALITY_ASSURANCE,
};

const supportedTrackValues = _.uniq([

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
Using _.uniq here is unnecessary since ChallengeTrackEnum and legacyTrackAliases are distinct by design. Consider removing _.uniq to simplify the code.

if (_.isNil(track)) {
return track;
}
const normalized = _.toUpper(_.trim(track));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The use of _.toUpper and _.trim is appropriate for normalizing input, but ensure that track values are consistently formatted before being passed to this function to avoid unnecessary processing.

await checkTrackName(type.name);
await checkTrackAbrv(type.abbreviation);
const normalizedTrack = normalizeTrackValue(type.track);
let ret = await prisma.challengeTrack.create({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Ensure that normalizeTrackValue handles all potential edge cases, such as unexpected input types, to prevent runtime errors.

};
const challengeTimelineTemplates = template.challengeTimelineTemplates || [];

if (!challengeTimelineTemplates.length) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
Consider using _.isEmpty(challengeTimelineTemplates) instead of checking !challengeTimelineTemplates.length for better readability and to avoid potential issues with falsy values.


return _.map(challengeTimelineTemplates, (challengeTimelineTemplate) => ({
...baseTemplate,
isDefault: challengeTimelineTemplate.isDefault === true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The expression challengeTimelineTemplate.isDefault === true is redundant. Consider using Boolean(challengeTimelineTemplate.isDefault) for clarity and to ensure the value is explicitly converted to a boolean.

await prisma.challenge.deleteMany({
where: {id}
})
const idsToDelete = _.compact([id, id2])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of _.compact([id, id2]) is a good approach to filter out falsy values, but ensure that id and id2 are initialized to null or undefined if they are not set elsewhere to avoid unexpected behavior.


it('create challenge successfully when project directProjectId is a numeric string', async () => {
const challengeData = _.cloneDeep(testChallengeData)
const originalGetProject = projectHelper.getProject

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding projectHelper.getProject could lead to side effects if other tests rely on this method. Consider using a mocking library like sinon to stub this method instead, ensuring isolation between tests.

}
})))

prisma.memberChallengeAccess.findMany = async () =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Overriding prisma.memberChallengeAccess.findMany directly could lead to side effects if other tests rely on this method. Consider using a mocking library like sinon to stub this method instead, ensuring isolation between tests.

let originalFindUnique
let originalUpdate

beforeEach(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Consider using a mocking library like sinon to stub prisma methods instead of manually reassigning them. This can improve test isolation and maintainability.

prisma.challengeTrack.update = originalUpdate
})

it('create challenge track - accepts DEVELOP alias', async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The test relies on the assumption that the createChallengeTrack method will transform the track value from 'DEVELOP' to 'DEVELOPMENT'. Ensure that this transformation logic is implemented and tested separately to avoid false positives in this test.

should.equal(result.track, 'DEVELOPMENT')
})

it('search challenge tracks - accepts QA alias', async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The test relies on the assumption that the searchChallengeTracks method will transform the track value from 'QA' to 'QUALITY_ASSURANCE'. Ensure that this transformation logic is implemented and tested separately to avoid false positives in this test.

should.equal(receivedFilter.track.equals, 'QUALITY_ASSURANCE')
})

it('partially update challenge track - accepts QA alias', async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The test relies on the assumption that the partiallyUpdateChallengeTrack method will transform the track value from 'QA' to 'QUALITY_ASSURANCE'. Ensure that this transformation logic is implemented and tested separately to avoid false positives in this test.

await prisma.challengeType.create({
data: {
id: challengeTypeId,
name: `type-${new Date().getTime()}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using new Date().getTime() multiple times in quick succession can lead to duplicate values if the operations are executed within the same millisecond. Consider using a single timestamp variable for consistency.

await prisma.challengeTrack.create({
data: {
id: challengeTrackId,
name: `track-${new Date().getTime()}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using new Date().getTime() multiple times in quick succession can lead to duplicate values if the operations are executed within the same millisecond. Consider using a single timestamp variable for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants