Skip to content

Commit da4e652

Browse files
committed
Keep phase IDs when updating and fix challenge authorization check to allow challenge creators to update (regardless of challenge role)
1 parent 7f25c61 commit da4e652

File tree

3 files changed

+147
-4
lines changed

3 files changed

+147
-4
lines changed

src/common/helper.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,10 +1079,18 @@ async function ensureUserCanModifyChallenge(currentUser, challenge, challengeRes
10791079
currentUser.userId,
10801080
challengeResources
10811081
);
1082+
const challengeCreator = _.isNil(challenge.createdBy)
1083+
? null
1084+
: _.toString(challenge.createdBy);
1085+
const currentUserId = _.isNil(currentUser.userId) ? null : _.toString(currentUser.userId);
1086+
const currentUserHandle = currentUser.handle ? _.toLower(currentUser.handle) : null;
1087+
const isChallengeCreator =
1088+
(!!challengeCreator && !!currentUserId && challengeCreator === currentUserId) ||
1089+
(!!challengeCreator && !!currentUserHandle && _.toLower(challengeCreator) === currentUserHandle);
10821090
if (
10831091
!currentUser.isMachine &&
10841092
!hasAdminRole(currentUser) &&
1085-
challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase() &&
1093+
!isChallengeCreator &&
10861094
!isUserHasFullAccess
10871095
) {
10881096
throw new errors.ForbiddenError(

src/services/ChallengeService.js

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,8 @@ async function updateChallenge(currentUser, challengeId, data) {
18221822
}
18231823
enrichChallengeForResponse(challenge);
18241824
prismaHelper.convertModelToResponse(challenge);
1825+
const originalChallengePhases = _.cloneDeep(challenge.phases || []);
1826+
const auditUserId = _.toString(currentUser.userId);
18251827
const existingPrizeType = challengeHelper.validatePrizeSetsAndGetPrizeType(challenge.prizeSets);
18261828

18271829
// No conversion needed - values are already in dollars in the database
@@ -2108,6 +2110,7 @@ async function updateChallenge(currentUser, challengeId, data) {
21082110
}
21092111

21102112
let phasesUpdated = false;
2113+
let phasesForUpdate = null;
21112114
if (
21122115
((data.phases && data.phases.length > 0) ||
21132116
isChallengeBeingActivated ||
@@ -2140,10 +2143,12 @@ async function updateChallenge(currentUser, challengeId, data) {
21402143
}
21412144
phasesUpdated = true;
21422145
data.phases = newPhases;
2146+
phasesForUpdate = _.cloneDeep(newPhases);
21432147
}
21442148
if (isChallengeBeingCancelled && challenge.phases && challenge.phases.length > 0) {
21452149
data.phases = phaseHelper.handlePhasesAfterCancelling(challenge.phases);
21462150
phasesUpdated = true;
2151+
phasesForUpdate = _.cloneDeep(data.phases);
21472152
}
21482153
const phasesForDates = phasesUpdated ? data.phases : challenge.phases;
21492154

@@ -2264,6 +2269,9 @@ async function updateChallenge(currentUser, challengeId, data) {
22642269
delete data.phases;
22652270
}
22662271
}
2272+
if (_.isNil(data.phases)) {
2273+
phasesForUpdate = null;
2274+
}
22672275

22682276
// Normalize and validate reviewers' phase references before converting to Prisma input
22692277
if (!_.isNil(data.reviewers)) {
@@ -2318,6 +2326,9 @@ async function updateChallenge(currentUser, challengeId, data) {
23182326
updateData.updatedBy = _.toString(currentUser.userId);
23192327
// reset createdBy
23202328
delete updateData.createdBy;
2329+
if (!_.isNil(updateData.phases)) {
2330+
delete updateData.phases;
2331+
}
23212332

23222333
const newPrizeType = challengeHelper.validatePrizeSetsAndGetPrizeType(updateData.prizeSets);
23232334
if (newPrizeType != null && existingPrizeType != null && newPrizeType !== existingPrizeType) {
@@ -2326,6 +2337,15 @@ async function updateChallenge(currentUser, challengeId, data) {
23262337
);
23272338
}
23282339
const updatedChallenge = await prisma.$transaction(async (tx) => {
2340+
if (Array.isArray(phasesForUpdate)) {
2341+
await syncChallengePhases(
2342+
tx,
2343+
challengeId,
2344+
phasesForUpdate,
2345+
auditUserId,
2346+
originalChallengePhases
2347+
);
2348+
}
23292349
// drop nested data if updated
23302350
if (!_.isNil(updateData.legacyRecord)) {
23312351
await tx.challengeLegacy.deleteMany({ where: { challengeId } });
@@ -2345,9 +2365,6 @@ async function updateChallenge(currentUser, challengeId, data) {
23452365
if (!_.isNil(updateData.metadata)) {
23462366
await tx.challengeMetadata.deleteMany({ where: { challengeId } });
23472367
}
2348-
if (!_.isNil(updateData.phases)) {
2349-
await tx.challengePhase.deleteMany({ where: { challengeId } });
2350-
}
23512368
if (!_.isNil(updateData.prizeSets)) {
23522369
await tx.challengePrizeSet.deleteMany({ where: { challengeId } });
23532370
}
@@ -2778,6 +2795,117 @@ function sanitizeChallenge(challenge) {
27782795
return sanitized;
27792796
}
27802797

2798+
async function syncChallengePhases(tx, challengeId, updatedPhases, auditUserId, originalPhases = []) {
2799+
if (!Array.isArray(updatedPhases)) {
2800+
return;
2801+
}
2802+
2803+
const originalById = new Map((originalPhases || []).map((phase) => [phase.id, phase]));
2804+
const originalByPhaseId = new Map();
2805+
for (const original of originalPhases || []) {
2806+
if (!_.isNil(original.phaseId)) {
2807+
originalByPhaseId.set(original.phaseId, original);
2808+
}
2809+
}
2810+
const retainedIds = new Set();
2811+
2812+
for (const phase of updatedPhases) {
2813+
if (!phase) continue;
2814+
2815+
let recordId = !_.isNil(phase.id) ? phase.id : undefined;
2816+
let phaseDefinitionId = !_.isNil(phase.phaseId) ? phase.phaseId : undefined;
2817+
2818+
if (!phaseDefinitionId && recordId && originalById.has(recordId)) {
2819+
phaseDefinitionId = originalById.get(recordId).phaseId;
2820+
}
2821+
if (!recordId && phaseDefinitionId && originalByPhaseId.has(phaseDefinitionId)) {
2822+
recordId = originalByPhaseId.get(phaseDefinitionId).id;
2823+
}
2824+
if (!phaseDefinitionId && recordId && originalById.has(recordId)) {
2825+
phaseDefinitionId = originalById.get(recordId).phaseId;
2826+
}
2827+
if (!phaseDefinitionId) {
2828+
throw new BadRequestError("Cannot update challenge phases without phaseId");
2829+
}
2830+
if (!recordId) {
2831+
recordId = uuid();
2832+
}
2833+
2834+
const existing = originalById.get(recordId);
2835+
retainedIds.add(recordId);
2836+
2837+
const scalarKeys = [
2838+
"name",
2839+
"description",
2840+
"isOpen",
2841+
"duration",
2842+
"scheduledStartDate",
2843+
"scheduledEndDate",
2844+
"actualStartDate",
2845+
"actualEndDate",
2846+
"challengeSource",
2847+
];
2848+
const phaseData = {};
2849+
for (const key of scalarKeys) {
2850+
if (!_.isUndefined(phase[key])) {
2851+
phaseData[key] = phase[key];
2852+
}
2853+
}
2854+
phaseData.predecessor = _.isNil(phase.predecessor) ? null : phase.predecessor;
2855+
2856+
phaseData.phaseId = phaseDefinitionId;
2857+
2858+
if (existing) {
2859+
await tx.challengePhase.update({
2860+
where: { id: recordId },
2861+
data: {
2862+
...phaseData,
2863+
updatedBy: auditUserId,
2864+
},
2865+
});
2866+
await tx.challengePhaseConstraint.deleteMany({ where: { challengePhaseId: recordId } });
2867+
} else {
2868+
await tx.challengePhase.create({
2869+
data: {
2870+
id: recordId,
2871+
challengeId,
2872+
...phaseData,
2873+
createdBy: auditUserId,
2874+
updatedBy: auditUserId,
2875+
},
2876+
});
2877+
}
2878+
2879+
if (Array.isArray(phase.constraints) && phase.constraints.length > 0) {
2880+
for (const constraint of phase.constraints) {
2881+
if (_.isNil(constraint.name) || _.isNil(constraint.value)) {
2882+
continue;
2883+
}
2884+
2885+
const constraintData = {
2886+
challengePhaseId: recordId,
2887+
name: constraint.name,
2888+
value: constraint.value,
2889+
createdBy: auditUserId,
2890+
updatedBy: auditUserId,
2891+
};
2892+
2893+
if (!_.isNil(constraint.id)) {
2894+
constraintData.id = constraint.id;
2895+
}
2896+
2897+
await tx.challengePhaseConstraint.create({ data: constraintData });
2898+
}
2899+
}
2900+
}
2901+
2902+
for (const phase of originalPhases || []) {
2903+
if (!retainedIds.has(phase.id)) {
2904+
await tx.challengePhase.delete({ where: { id: phase.id } });
2905+
}
2906+
}
2907+
}
2908+
27812909
function sanitizeData(data, challenge) {
27822910
for (const key in data) {
27832911
if (key === "phases") continue;

test/unit/ChallengeService.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,13 @@ describe('challenge service unit tests', () => {
746746
should.exist(result.updated)
747747
})
748748

749+
it('update challenge - creator memberId can modify without matching handle', async () => {
750+
const updatePayload = { privateDescription: 'Creator update via memberId' }
751+
const result = await service.updateChallenge({ userId: 'testuser', handle: 'different-handle' }, id, updatePayload)
752+
should.equal(result.id, id)
753+
should.equal(result.privateDescription, updatePayload.privateDescription)
754+
})
755+
749756
it('update challenge - project not found', async () => {
750757
try {
751758
await service.updateChallenge(

0 commit comments

Comments
 (0)