Skip to content

Commit cc58a2d

Browse files
authored
Merge pull request #187 from topcoder-platform/develop
[PROD] - PS489
2 parents 88bc2e7 + 2d38b73 commit cc58a2d

File tree

10 files changed

+541
-328
lines changed

10 files changed

+541
-328
lines changed

pnpm-lock.yaml

Lines changed: 294 additions & 294 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/review-application/reviewApplication.service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ export class ReviewApplicationService {
7171
}
7272
// make sure application role matches
7373
if (
74-
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
7574
ReviewApplicationRoleOpportunityTypeMap[dto.role] !== opportunity.type
7675
) {
7776
throw new BadRequestException(

src/api/submission/submission.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ export class SubmissionController {
197197
this.logger.log(
198198
`Getting submissions with filters - ${JSON.stringify(queryDto)}`,
199199
);
200-
const authUser: JwtUser = req['user'] as JwtUser;
200+
const authUser: JwtUser =
201+
(req['user'] as JwtUser) ?? ({ isMachine: false, roles: [] } as JwtUser);
201202
return this.service.listSubmission(
202203
authUser,
203204
queryDto,

src/api/submission/submission.service.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ describe('SubmissionService', () => {
510510
reviewType: {
511511
findMany: jest.Mock;
512512
};
513+
$queryRaw: jest.Mock;
513514
};
514515
let prismaErrorServiceMock: { handleError: jest.Mock };
515516
let challengePrismaMock: {
@@ -537,9 +538,14 @@ describe('SubmissionService', () => {
537538
reviewType: {
538539
findMany: jest.fn().mockResolvedValue([]),
539540
},
541+
$queryRaw: jest.fn().mockResolvedValue([]),
540542
};
541543
prismaErrorServiceMock = {
542-
handleError: jest.fn(),
544+
handleError: jest.fn().mockReturnValue({
545+
message: 'Unexpected error',
546+
code: 'INTERNAL_ERROR',
547+
details: {},
548+
}),
543549
};
544550
challengePrismaMock = {
545551
$queryRaw: jest.fn().mockResolvedValue([]),
@@ -624,6 +630,7 @@ describe('SubmissionService', () => {
624630
prismaMock.submission.findFirst.mockResolvedValue({
625631
id: 'submission-new',
626632
});
633+
prismaMock.$queryRaw.mockResolvedValue([{ id: 'submission-new' }]);
627634

628635
const result = await listService.listSubmission(
629636
{ isMachine: false } as any,
@@ -1241,5 +1248,56 @@ describe('SubmissionService', () => {
12411248
expect(screeningReview?.reviewerHandle).toBe('screeningHandle');
12421249
expect(screeningReview?.reviewerMaxRating).toBe(2000);
12431250
});
1251+
1252+
it('exposes submitter identity but strips reviews for anonymous challenge queries', async () => {
1253+
const now = new Date('2025-02-01T12:00:00Z');
1254+
const submissions = [
1255+
{
1256+
id: 'submission-anon',
1257+
challengeId: 'challenge-1',
1258+
memberId: '101',
1259+
submittedDate: now,
1260+
createdAt: now,
1261+
updatedAt: now,
1262+
type: SubmissionType.CONTEST_SUBMISSION,
1263+
status: SubmissionStatus.ACTIVE,
1264+
review: [{ id: 'review-public', score: 100 }],
1265+
reviewSummation: [{ id: 'summation-public' }],
1266+
url: 'https://example.com/submission.zip',
1267+
legacyChallengeId: null,
1268+
prizeId: null,
1269+
},
1270+
];
1271+
1272+
prismaMock.submission.findMany.mockResolvedValue(
1273+
submissions.map((entry) => ({ ...entry })),
1274+
);
1275+
prismaMock.submission.count.mockResolvedValue(submissions.length);
1276+
prismaMock.submission.findFirst.mockResolvedValue({
1277+
id: 'submission-anon',
1278+
});
1279+
1280+
memberPrismaMock.member.findMany.mockResolvedValue([
1281+
{
1282+
userId: BigInt(101),
1283+
handle: 'anonUser',
1284+
maxRating: { rating: 1500 },
1285+
},
1286+
]);
1287+
1288+
const result = await listService.listSubmission(
1289+
{ isMachine: false, roles: [] } as any,
1290+
{ challengeId: 'challenge-1' } as any,
1291+
{ page: 1, perPage: 10 } as any,
1292+
);
1293+
1294+
const submissionResult = result.data[0];
1295+
expect(submissionResult.memberId).toBe('101');
1296+
expect(submissionResult.submitterHandle).toBe('anonUser');
1297+
expect(submissionResult.submitterMaxRating).toBe(1500);
1298+
expect(submissionResult).not.toHaveProperty('review');
1299+
expect(submissionResult).not.toHaveProperty('reviewSummation');
1300+
expect(submissionResult.url).toBeNull();
1301+
});
12441302
});
12451303
});

src/api/submission/submission.service.ts

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,9 +1812,9 @@ export class SubmissionService {
18121812
: undefined;
18131813

18141814
if (requestedMemberId) {
1815-
const userId = authUser.userId ? String(authUser.userId) : undefined;
1815+
const userId = authUser?.userId ? String(authUser.userId) : undefined;
18161816
const isRequestingMember = userId === requestedMemberId;
1817-
const hasCopilotRole = (authUser.roles ?? []).includes(
1817+
const hasCopilotRole = (authUser?.roles ?? []).includes(
18181818
UserRole.Copilot,
18191819
);
18201820
const hasElevatedAccess = isAdmin(authUser) || hasCopilotRole;
@@ -1862,7 +1862,7 @@ export class SubmissionService {
18621862
: '';
18631863

18641864
let restrictedChallengeIds = new Set<string>();
1865-
if (!isPrivilegedRequester && requesterUserId) {
1865+
if (!isPrivilegedRequester && requesterUserId && !queryDto.challengeId) {
18661866
try {
18671867
restrictedChallengeIds =
18681868
await this.getActiveSubmitterRestrictedChallengeIds(
@@ -1885,7 +1885,8 @@ export class SubmissionService {
18851885
if (
18861886
!isPrivilegedRequester &&
18871887
requesterUserId &&
1888-
restrictedChallengeIds.size
1888+
restrictedChallengeIds.size &&
1889+
!queryDto.challengeId
18891890
) {
18901891
const restrictedList = Array.from(restrictedChallengeIds);
18911892
const restrictionCriteria: Prisma.submissionWhereInput = {
@@ -1928,12 +1929,16 @@ export class SubmissionService {
19281929
orderBy,
19291930
});
19301931

1931-
// Enrich with submitter handle and max rating for authorized callers
1932-
const canViewSubmitter = await this.canViewSubmitterIdentity(
1933-
authUser,
1934-
queryDto.challengeId,
1935-
);
1936-
if (canViewSubmitter && submissions.length) {
1932+
// Enrich with submitter handle and max rating (always for challenge listings)
1933+
const shouldEnrichSubmitter =
1934+
submissions.length > 0 &&
1935+
(queryDto.challengeId
1936+
? true
1937+
: await this.canViewSubmitterIdentity(
1938+
authUser,
1939+
queryDto.challengeId,
1940+
));
1941+
if (shouldEnrichSubmitter) {
19371942
try {
19381943
const memberIds = Array.from(
19391944
new Set(
@@ -1943,11 +1948,24 @@ export class SubmissionService {
19431948
),
19441949
);
19451950
if (memberIds.length) {
1946-
const idsAsBigInt = memberIds.map((id) => BigInt(id));
1947-
const members = await this.memberPrisma.member.findMany({
1948-
where: { userId: { in: idsAsBigInt } },
1949-
include: { maxRating: true },
1950-
});
1951+
const idsAsBigInt: bigint[] = [];
1952+
for (const id of memberIds) {
1953+
try {
1954+
idsAsBigInt.push(BigInt(id));
1955+
} catch (error) {
1956+
this.logger.debug(
1957+
`[listSubmission] Skipping submitter ${id}: unable to convert to BigInt. ${error}`,
1958+
);
1959+
}
1960+
}
1961+
1962+
const members =
1963+
idsAsBigInt.length > 0
1964+
? await this.memberPrisma.member.findMany({
1965+
where: { userId: { in: idsAsBigInt } },
1966+
include: { maxRating: true },
1967+
})
1968+
: [];
19511969
const map = new Map<
19521970
string,
19531971
{ handle: string; maxRating: number | null }
@@ -2001,11 +2019,6 @@ export class SubmissionService {
20012019
submissions,
20022020
reviewVisibilityContext,
20032021
);
2004-
this.stripSubmitterMemberIds(
2005-
authUser,
2006-
submissions,
2007-
reviewVisibilityContext,
2008-
);
20092022
await this.stripIsLatestForUnlimitedChallenges(submissions);
20102023

20112024
this.logger.log(
@@ -2408,25 +2421,43 @@ export class SubmissionService {
24082421
return emptyContext;
24092422
}
24102423

2424+
const requesterUserId =
2425+
authUser?.userId !== undefined && authUser?.userId !== null
2426+
? String(authUser.userId).trim()
2427+
: '';
2428+
24112429
const isPrivilegedRequester = authUser?.isMachine || isAdmin(authUser);
2430+
if (!isPrivilegedRequester && !requesterUserId) {
2431+
for (const submission of submissions) {
2432+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
2433+
delete (submission as any).review;
2434+
}
2435+
if (
2436+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
2437+
) {
2438+
delete (submission as any).reviewSummation;
2439+
}
2440+
}
2441+
return {
2442+
...emptyContext,
2443+
requesterUserId,
2444+
};
2445+
}
2446+
24122447
if (isPrivilegedRequester) {
2413-
const requesterUserId =
2414-
authUser?.userId !== undefined && authUser?.userId !== null
2415-
? String(authUser.userId).trim()
2416-
: '';
24172448
return {
24182449
...emptyContext,
24192450
requesterUserId,
24202451
};
24212452
}
24222453

2423-
const uid =
2424-
authUser?.userId !== undefined && authUser?.userId !== null
2425-
? String(authUser.userId).trim()
2426-
: '';
2454+
const uid = requesterUserId;
24272455

24282456
if (!uid) {
2429-
return emptyContext;
2457+
return {
2458+
...emptyContext,
2459+
requesterUserId,
2460+
};
24302461
}
24312462

24322463
const challengeIds = Array.from(
@@ -3397,7 +3428,26 @@ export class SubmissionService {
33973428
}
33983429

33993430
const uid = visibilityContext.requesterUserId;
3431+
this.logger.debug(
3432+
`[stripSubmitterSubmissionDetails] requesterUserId=${uid ?? '<undefined>'}`,
3433+
);
34003434
if (!uid) {
3435+
this.logger.debug(
3436+
'[stripSubmitterSubmissionDetails] Anonymized requester; removing review metadata and URLs.',
3437+
);
3438+
for (const submission of submissions) {
3439+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
3440+
delete (submission as any).review;
3441+
}
3442+
if (
3443+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
3444+
) {
3445+
delete (submission as any).reviewSummation;
3446+
}
3447+
if (Object.prototype.hasOwnProperty.call(submission, 'url')) {
3448+
(submission as any).url = null;
3449+
}
3450+
}
34013451
return;
34023452
}
34033453

@@ -3472,6 +3522,19 @@ export class SubmissionService {
34723522

34733523
const uid = visibilityContext.requesterUserId;
34743524
if (!uid) {
3525+
for (const submission of submissions) {
3526+
if (Object.prototype.hasOwnProperty.call(submission, 'review')) {
3527+
delete (submission as any).review;
3528+
}
3529+
if (
3530+
Object.prototype.hasOwnProperty.call(submission, 'reviewSummation')
3531+
) {
3532+
delete (submission as any).reviewSummation;
3533+
}
3534+
if (Object.prototype.hasOwnProperty.call(submission, 'url')) {
3535+
(submission as any).url = null;
3536+
}
3537+
}
34753538
return;
34763539
}
34773540

src/dto/reviewApplication.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import type { ReviewOpportunityType as PrismaReviewOpportunityType } from '@prisma/client';
23
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
34
import { ReviewOpportunityType } from './reviewOpportunity.dto';
45

@@ -37,7 +38,7 @@ export const ReviewApplicationRoleIds: Record<ReviewApplicationRole, number> = {
3738
// read from review_application_role_lu.review_auction_type_id
3839
export const ReviewApplicationRoleOpportunityTypeMap: Record<
3940
ReviewApplicationRole,
40-
ReviewOpportunityType
41+
PrismaReviewOpportunityType
4142
> = {
4243
PRIMARY_REVIEWER: ReviewOpportunityType.COMPONENT_DEV_REVIEW,
4344
SECONDARY_REVIEWER: ReviewOpportunityType.COMPONENT_DEV_REVIEW,

src/shared/guards/tokenRoles.guard.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('TokenRolesGuard', () => {
2020
type TestRequest = Record<string, unknown> & {
2121
method: string;
2222
query: Record<string, unknown>;
23-
user: {
23+
user?: {
2424
userId: string;
2525
isMachine: boolean;
2626
roles?: unknown[];
@@ -132,5 +132,16 @@ describe('TokenRolesGuard', () => {
132132

133133
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
134134
});
135+
136+
it('allows anonymous access when challengeId is provided', () => {
137+
const request = {
138+
method: 'GET',
139+
query: { challengeId: '12345' },
140+
};
141+
142+
const context = createExecutionContext(request as TestRequest);
143+
144+
expect(guard.canActivate(context)).toBe(true);
145+
});
135146
});
136147
});

0 commit comments

Comments
 (0)