Conversation
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
Show autofix suggestion
Hide autofix suggestion
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 computeCORS_ALLOWED_ORIGINSfromconfig(e.g., an array or a comma‑separated string). - Replace
origin: "*"with anorigincallback that:- Allows requests with no
Origin(non‑browser or same‑origin requests) by invoking the callback withnull. - 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).
- Allows requests with no
All changes will be confined to app.js around the CORS configuration, without touching other middleware or routes.
| @@ -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", |
|
|
||
| const PhaseChangeNotificationSettings = { | ||
| PHASE_CHANGE: { | ||
| sendgridTemplateId: config.PHASE_CHANGE_SENDGRID_TEMPLATE_ID, |
There was a problem hiding this comment.
[❗❗ 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: "*", |
There was a problem hiding this comment.
[❗❗ 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' , |
There was a problem hiding this comment.
[❗❗ 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 || "", |
There was a problem hiding this comment.
[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(); |
There was a problem hiding this comment.
[💡 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)}`, { |
There was a problem hiding this comment.
[❗❗ 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) : []; |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[reliability]
Consider adding a retry mechanism for postBusEvent to handle transient network errors more gracefully.
| await postChallengeUpdatedNotification(challengeId); | ||
|
|
||
| // send notification logic | ||
| try { |
There was a problem hiding this comment.
[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({ |
There was a problem hiding this comment.
[❗❗ 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); |
There was a problem hiding this comment.
[❗❗ 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); |
There was a problem hiding this comment.
[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([ |
There was a problem hiding this comment.
[💡 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)); |
There was a problem hiding this comment.
[💡 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({ |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[💡 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, |
There was a problem hiding this comment.
[💡 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]) |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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 () => |
There was a problem hiding this comment.
[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(() => { |
There was a problem hiding this comment.
[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 () => { |
There was a problem hiding this comment.
[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 () => { |
There was a problem hiding this comment.
[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 () => { |
There was a problem hiding this comment.
[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()}`, |
There was a problem hiding this comment.
[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()}`, |
There was a problem hiding this comment.
[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.
@jmgasper please fill in.