From df0ed543d2346b76b3401817ac77f521ef7bd4ad Mon Sep 17 00:00:00 2001 From: hjkim24 Date: Wed, 19 Nov 2025 06:39:20 +0000 Subject: [PATCH 1/3] feat(be): implement toggleParticipantBlocking --- .../admin/src/contest/contest.resolver.ts | 13 ++++++ .../apps/admin/src/contest/contest.service.ts | 45 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/apps/backend/apps/admin/src/contest/contest.resolver.ts b/apps/backend/apps/admin/src/contest/contest.resolver.ts index bfd78e510e..1b63f14183 100644 --- a/apps/backend/apps/admin/src/contest/contest.resolver.ts +++ b/apps/backend/apps/admin/src/contest/contest.resolver.ts @@ -204,6 +204,19 @@ export class ContestResolver { ) { return await this.contestService.getContestParticipants(contestId) } + + @Mutation(() => UserContest) + async toggleParticipantBlocking( + @Context('req') req: AuthenticatedRequest, + @Args('userId', { type: () => Int }) targetUserId: number, + @Args('contestId', { type: () => Int }) contestId: number + ) { + return await this.contestService.toggleParticipantBlocking( + req.user, + targetUserId, + contestId + ) + } } @Resolver(() => ContestProblem) diff --git a/apps/backend/apps/admin/src/contest/contest.service.ts b/apps/backend/apps/admin/src/contest/contest.service.ts index cda1434aee..f070fc04b7 100644 --- a/apps/backend/apps/admin/src/contest/contest.service.ts +++ b/apps/backend/apps/admin/src/contest/contest.service.ts @@ -4,6 +4,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter' import { Contest, ResultStatus, Submission } from '@generated' import { ContestRole, Prisma, Role } from '@prisma/client' import { Cache } from 'cache-manager' +import type { AuthenticatedUser } from '@libs/auth' import { MAX_DATE } from '@libs/constants' import { EntityNotExistException, @@ -1371,4 +1372,48 @@ export class ContestService { isBlocked })) } + + async toggleParticipantBlocking( + user: AuthenticatedUser, + targetUserId: number, + contestId: number + ) { + const [userContest, contestStaff] = await Promise.all([ + this.prisma.userContest.findFirst({ + where: { + userId: targetUserId, + contestId + } + }), + this.prisma.userContest.findFirst({ + where: { + userId: user.id, + contestId, + role: { in: [ContestRole.Admin, ContestRole.Manager] } + }, + select: { role: true } + }) + ]) + + if (!userContest) { + throw new EntityNotExistException('UserContest') + } + + if (userContest.role != ContestRole.Participant) { + throw new UnprocessableDataException( + 'Only can block Contest Participant.' + ) + } + + if (!contestStaff && !user.isSuperAdmin()) { + throw new ForbiddenAccessException( + 'Only Contest Admin or Manager can modify block status.' + ) + } + + return await this.prisma.userContest.update({ + where: { id: userContest.id }, + data: { isBlocked: !userContest.isBlocked } + }) + } } From 5e07bf6a6670fc086f8e2b1cc96e9e1abdda7b60 Mon Sep 17 00:00:00 2001 From: hjkim24 Date: Wed, 19 Nov 2025 06:39:40 +0000 Subject: [PATCH 2/3] docs(be): add bruno testcases --- apps/backend/schema.gql | 1 + .../Toggle Block Participant/Succeed.bru | 87 +++++++++++++++++++ .../[ERR] Nonexistent UserContest.bru | 87 +++++++++++++++++++ ...RR] Only can block Contest Participant.bru | 87 +++++++++++++++++++ .../Toggle Block Participant/folder.bru | 8 ++ collection/admin/login.js | 8 ++ 6 files changed, 278 insertions(+) create mode 100644 collection/admin/Contest/Toggle Block Participant/Succeed.bru create mode 100644 collection/admin/Contest/Toggle Block Participant/[ERR] Nonexistent UserContest.bru create mode 100644 collection/admin/Contest/Toggle Block Participant/[ERR] Only can block Contest Participant.bru create mode 100644 collection/admin/Contest/Toggle Block Participant/folder.bru diff --git a/apps/backend/schema.gql b/apps/backend/schema.gql index dddc41e673..b4ab56f6b5 100644 --- a/apps/backend/schema.gql +++ b/apps/backend/schema.gql @@ -2166,6 +2166,7 @@ type Mutation { removeUserFromContest(contestId: Int!, userId: Int!): UserContest! revokeInvitation(groupId: Int!): String! toggleContestQnAResolved(contestId: Int!, qnAOrder: Int!): ContestQnA! + toggleParticipantBlocking(contestId: Int!, userId: Int!): UserContest! updateAnnouncement(contestId: Int!, input: UpdateAnnouncementInput!): Announcement! updateAssignment(groupId: Int!, input: UpdateAssignmentInput!): Assignment! updateAssignmentProblemRecord(groupId: Int!, input: UpdateAssignmentProblemRecordInput!): AssignmentProblemRecord! diff --git a/collection/admin/Contest/Toggle Block Participant/Succeed.bru b/collection/admin/Contest/Toggle Block Participant/Succeed.bru new file mode 100644 index 0000000000..0fc02d4bd2 --- /dev/null +++ b/collection/admin/Contest/Toggle Block Participant/Succeed.bru @@ -0,0 +1,87 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: inherit +} + +body:graphql { + mutation ToggleParticipantBlocking( + $userId: Int!, + $contestId: Int! + ) { + toggleParticipantBlocking( + userId: $userId + contestId: $contestId + ) { + id + role + userId + contestId + isBlocked + } + } +} + +body:graphql:vars { + { + "userId" : 7, + "contestId" : 19 + } +} + +script:pre-request { + await require("./login").loginContestAdmin(req); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + # πŸ“˜ Toggle Block Participant + + **POST** `/graphql (mutation: toggleBlockParticipant)` + + λŒ€νšŒ μ°Έκ°€μžμ— λŒ€ν•΄ block 토글을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + + --- + + ### πŸ”’ Authentication + + βœ… Required (Contest Admin / Manager, SuperAdmin) + + --- + + ## μš”μ²­ ν•„λ“œ + | ν•„λ“œλͺ… | νƒ€μž… | ν•„μˆ˜ μ—¬λΆ€ | μ„€λͺ… | + |--------|------|----------|------| + | `userId` | Int | βœ… | μ‚¬μš©μž Id | + | `contestId` | Int | βœ… | λŒ€νšŒ Id | + + --- + + ### πŸ“€ Response Body + + #### Content-Type: `application/json` + + ```json + { + "data": { + "toggleParticipantBlocking": { + "id": "47", + "role": "Participant", + "userId": 7, + "contestId": 19, + "isBlocked": false + } + } + } + ``` +} diff --git a/collection/admin/Contest/Toggle Block Participant/[ERR] Nonexistent UserContest.bru b/collection/admin/Contest/Toggle Block Participant/[ERR] Nonexistent UserContest.bru new file mode 100644 index 0000000000..5f179e0ef1 --- /dev/null +++ b/collection/admin/Contest/Toggle Block Participant/[ERR] Nonexistent UserContest.bru @@ -0,0 +1,87 @@ +meta { + name: [ERR] Nonexistent UserContest + type: graphql + seq: 2 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: inherit +} + +body:graphql { + mutation ToggleParticipantBlocking( + $userId: Int!, + $contestId: Int! + ) { + toggleParticipantBlocking( + userId: $userId + contestId: $contestId + ) { + id + role + userId + contestId + isBlocked + } + } +} + +body:graphql:vars { + { + "userId" : 999, + "contestId" : 19 + } +} + +script:pre-request { + await require("./login").loginContestAdmin(req); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + # πŸ“˜ Toggle Block Participant + + **POST** `/graphql (mutation: toggleBlockParticipant)` + + λŒ€νšŒ μ°Έκ°€μžμ— λŒ€ν•΄ block 토글을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + + --- + + ### πŸ”’ Authentication + + βœ… Required (Contest Admin / Manager, SuperAdmin) + + --- + + ## μš”μ²­ ν•„λ“œ + | ν•„λ“œλͺ… | νƒ€μž… | ν•„μˆ˜ μ—¬λΆ€ | μ„€λͺ… | + |--------|------|----------|------| + | `userId` | Int | βœ… | μ‚¬μš©μž Id | + | `contestId` | Int | βœ… | λŒ€νšŒ Id | + + --- + + ### πŸ“€ Response Body + + #### Content-Type: `application/json` + + ```json + { + "data": { + "toggleParticipantBlocking": { + "id": "47", + "role": "Participant", + "userId": 7, + "contestId": 19, + "isBlocked": false + } + } + } + ``` +} diff --git a/collection/admin/Contest/Toggle Block Participant/[ERR] Only can block Contest Participant.bru b/collection/admin/Contest/Toggle Block Participant/[ERR] Only can block Contest Participant.bru new file mode 100644 index 0000000000..51ecc53f9f --- /dev/null +++ b/collection/admin/Contest/Toggle Block Participant/[ERR] Only can block Contest Participant.bru @@ -0,0 +1,87 @@ +meta { + name: [ERR] Only can block Contest Participant + type: graphql + seq: 3 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: inherit +} + +body:graphql { + mutation ToggleParticipantBlocking( + $userId: Int!, + $contestId: Int! + ) { + toggleParticipantBlocking( + userId: $userId + contestId: $contestId + ) { + id + role + userId + contestId + isBlocked + } + } +} + +body:graphql:vars { + { + "userId" : 4, + "contestId" : 19 + } +} + +script:pre-request { + await require("./login").loginContestAdmin(req); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + # πŸ“˜ Toggle Block Participant + + **POST** `/graphql (mutation: toggleBlockParticipant)` + + λŒ€νšŒ μ°Έκ°€μžμ— λŒ€ν•΄ block 토글을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + + --- + + ### πŸ”’ Authentication + + βœ… Required (Contest Admin / Manager, SuperAdmin) + + --- + + ## μš”μ²­ ν•„λ“œ + | ν•„λ“œλͺ… | νƒ€μž… | ν•„μˆ˜ μ—¬λΆ€ | μ„€λͺ… | + |--------|------|----------|------| + | `userId` | Int | βœ… | μ‚¬μš©μž Id | + | `contestId` | Int | βœ… | λŒ€νšŒ Id | + + --- + + ### πŸ“€ Response Body + + #### Content-Type: `application/json` + + ```json + { + "data": { + "toggleParticipantBlocking": { + "id": "47", + "role": "Participant", + "userId": 7, + "contestId": 19, + "isBlocked": false + } + } + } + ``` +} diff --git a/collection/admin/Contest/Toggle Block Participant/folder.bru b/collection/admin/Contest/Toggle Block Participant/folder.bru new file mode 100644 index 0000000000..1343c7cb05 --- /dev/null +++ b/collection/admin/Contest/Toggle Block Participant/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Toggle Block Participant + seq: 22 +} + +auth { + mode: inherit +} diff --git a/collection/admin/login.js b/collection/admin/login.js index 13c59a5783..c9f26437fb 100644 --- a/collection/admin/login.js +++ b/collection/admin/login.js @@ -12,6 +12,13 @@ const login = async (req, body) => { } } +const loginSuper = async (req) => { + await login(req, { + username: 'super', + password: 'Supersuper' + }) +} + const loginAdmin = async (req) => { await login(req, { username: 'admin', @@ -56,6 +63,7 @@ const loginUser = async (req) => { } module.exports = { + loginSuper, loginAdmin, loginContestAdmin, loginContestManager, From 6667796792f12cb29249e5d79846aa85e32c1d64 Mon Sep 17 00:00:00 2001 From: hjkim24 Date: Wed, 19 Nov 2025 07:00:08 +0000 Subject: [PATCH 3/3] chore(be): add isBlocked condition when posting ContestQnA --- apps/backend/apps/client/src/contest/contest.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/apps/client/src/contest/contest.service.ts b/apps/backend/apps/client/src/contest/contest.service.ts index f8c317e744..3c626e088f 100644 --- a/apps/backend/apps/client/src/contest/contest.service.ts +++ b/apps/backend/apps/client/src/contest/contest.service.ts @@ -643,7 +643,7 @@ export class ContestService { // λŒ€νšŒ μ§„ν–‰ 쀑인 경우 λŒ€νšŒμ— λ“±λ‘ν•œ μ°Έκ°€μž λ˜λŠ” κ΄€λ¦¬μžλ§Œ 질문 κ²Œμ‹œ κ°€λŠ₯ if (isOngoing) { const hasRegistered = await this.prisma.userContest.findFirst({ - where: { userId, contestId } + where: { userId, contestId, isBlocked: false } }) if (!hasRegistered) { const user = await this.prisma.user.findFirst({