From 3d864c34a875eaea5349d759e758de75c4a18ea5 Mon Sep 17 00:00:00 2001 From: SungHyun Do <52828205+glaxyt@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:52:10 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EC=86=8C=EC=BC=93=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EA=B0=80=EB=93=9C=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20API=20=EA=B5=AC=ED=98=84=20(#158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: redis exists 메서드 생성 * refactor: show quiz 책임 분리 * feat: 클래스가 비어있을 경우 빈 배열 반환 * feat: 퀴즈 다음 순서로 이동할 수 있게 추가 * feat: get class 오름차순 정렬 반환 * feat: 게임 상태 랭킹, 리더보드 추가 * feat: 실시간 소켓 서버 sid 가드 구현 * feat: 게이트웨이 가드 설정 --------- Co-authored-by: nowChae <99425616+nowChae@users.noreply.github.com> --- .../config/database/redis/redis.service.ts | 4 ++ .../src/module/game/games/game.gateway.ts | 49 ++++++++----------- .../server/src/module/guards/session.guard.ts | 29 +++++++++++ .../src/module/quiz/quizzes/quiz.service.ts | 4 -- .../quizzes/repositories/class.repository.ts | 11 +++-- packages/shared/types/gameStatus.types.ts | 2 + 6 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 packages/server/src/module/guards/session.guard.ts diff --git a/packages/server/src/config/database/redis/redis.service.ts b/packages/server/src/config/database/redis/redis.service.ts index 1bb45762..6b4e6cd8 100644 --- a/packages/server/src/config/database/redis/redis.service.ts +++ b/packages/server/src/config/database/redis/redis.service.ts @@ -14,6 +14,10 @@ export class RedisService { } } + async exists(key: string) { + return await this.redis.exists(key); + } + async get(key: string) { return await this.redis.get(key); } diff --git a/packages/server/src/module/game/games/game.gateway.ts b/packages/server/src/module/game/games/game.gateway.ts index 42517f71..fc4e0291 100644 --- a/packages/server/src/module/game/games/game.gateway.ts +++ b/packages/server/src/module/game/games/game.gateway.ts @@ -9,7 +9,7 @@ import { Server, Socket } from 'socket.io'; import { RedisService } from '../../../config/database/redis/redis.service'; import { v4 as uuidv4 } from 'uuid'; import { GameService } from './game.service'; -import { Injectable } from '@nestjs/common'; +import { Injectable, UseGuards } from '@nestjs/common'; import { MasterEntryRequestDto } from './dto/request/master-entry.request.dto'; import { ParticipantEntryRequestDto } from './dto/request/participant-entry.request.dto'; import { ShowQuizRequestDto } from './dto/request/show-quiz.request.dto'; @@ -29,6 +29,7 @@ import { import { CONVERT_TO_MS } from '@shared/constants/utils.constants'; import { CONNECTION_TYPES } from '@shared/types/connection.types'; import { GAMESTATUS_TYPES } from '@shared/types/gameStatus.types'; +import { SessionGuard } from '../../guards/session.guard'; @Injectable() @WebSocketGateway({ @@ -145,12 +146,14 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return participantSid; } + @UseGuards(SessionGuard) @SubscribeMessage('participant notice') async handleParticipantNotice(client: Socket, dto) { const { pinCode } = dto; client.to(pinCode).emit('participant notice'); } + @UseGuards(SessionGuard) @SubscribeMessage('participant info') async handleNickname(client: Socket, dto) { const { pinCode, sid } = dto; @@ -167,6 +170,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return { myPosition, participantList }; } + + @UseGuards(SessionGuard) @SubscribeMessage('start quiz') async handleStartQuiz(client: Socket, payload: StartQuizRequestDto) { const { sid, pinCode } = payload; @@ -196,7 +201,6 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { const currentQuizData = quizData[updatedCurrentOrder]; const choicesLength = currentQuizData['choices'].length; - const choiceStatus = Object.fromEntries( Array.from({ length: choicesLength }, (_, i) => [i, 0]), ); @@ -222,13 +226,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return { isStarted: true }; } - // @SubscribeMessage('time end') - // async handleTimeEnd(client: Socket, payload: any) { - // const { pinCode } = payload; - // const gameInfo = JSON.parse(await this.redisService.get(`gameId=${pinCode}`)); - // await this.redisService.set(`gameId=${pinCode}`, JSON.stringify(gameInfo)); - // } + @UseGuards(SessionGuard) @SubscribeMessage('show quiz') async handleShowQuiz(client: Socket, payload: ShowQuizRequestDto) { const { pinCode } = payload; @@ -245,35 +244,15 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { const isLast = gameInfo.currentOrder === quizMaxNum ? true : false; - console.log('show quiz before', client.id, currentOrder); // 기존에 퀴즈가 저장된적이 있는지. start quiz에 저장되어있음 const quizRedis = JSON.parse( await this.redisService.get(`gameId=${pinCode}:quizId=${currentOrder}`), ); - console.log('show quiz', client.id, quizRedis); const startTime = quizRedis.startTime; return { quizMaxNum, currentQuizData, startTime, isLast }; - // await this.intervalTimeSender(pinCode, startTime, currentTimeLimit); } - // async intervalTimeSender(pinCode: string, startTime: number, timeLimit: number) { - // const intervalId = setInterval(async () => { - // const currentTime = Date.now(); - // const elapsedTime = currentTime - startTime; - // const remainingTime = (timeLimit + QUIZ_WAITING_TIME) * 1000 - elapsedTime; - // if (remainingTime <= 0) { - // const gameInfo = JSON.parse(await this.redisService.get(`gameId=${pinCode}`)); - - // gameInfo.currentOrder += 1; - // await this.redisService.set(`gameId=${pinCode}`, JSON.stringify(gameInfo)); - // this.server.to(pinCode).emit('time end', { isEnd: true }); - // clearInterval(intervalId); - // return; - // } - // this.server.to(pinCode).emit('timer tick', { currentTime, elapsedTime, remainingTime }); - // }, INTERVAL_TIME); - // } private async storeQuizToRedis(classId: number) { const cachedQuizData = await this.redisService.get(`classId=${classId}`); @@ -290,6 +269,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return quizData; } + @UseGuards(SessionGuard) @SubscribeMessage('submit answer') async handleSubmitAnswer(client: Socket, payload: SubmitAnswerRequestDto) { const { pinCode, sid, selectedAnswer, submitTime } = payload; @@ -363,6 +343,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return { submitOrder: totalSubmit }; } + @UseGuards(SessionGuard) @SubscribeMessage('emoji') async handleEmoji(client: Socket, payload: EmojiRequestDto) { const { pinCode, currentOrder, emoji } = payload; @@ -386,6 +367,15 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return 0; } + @UseGuards(SessionGuard) + @SubscribeMessage('time end') + async handleTimeEnd(client: Socket, payload: any) { + const { pinCode } = payload; + const gameInfo = JSON.parse(await this.redisService.get(`gameId=${pinCode}`)); + await this.redisService.set(`gameId=${pinCode}`, JSON.stringify(gameInfo)); + } + + @UseGuards(SessionGuard) @SubscribeMessage('show ranking') async handleShowRanking(client: Socket, payload: ShowRankingRequestDto) { const { pinCode, sid } = payload; @@ -413,6 +403,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return showRankingData; } + @UseGuards(SessionGuard) @SubscribeMessage('end quiz') async handleEndQuiz(client: Socket, payload: EndQuizRequestDto) { const { sid, pinCode } = payload; @@ -444,6 +435,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return equals(selectedAnswer, correctAnswers); } + @UseGuards(SessionGuard) @SubscribeMessage('leaderboard') async handleLeaderboard(client: Socket, payload: LeaderboardRequestDto) { const { pinCode } = payload; @@ -472,6 +464,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { return leaderboardData; } + @UseGuards(SessionGuard) @SubscribeMessage('message') async handleMessage(client: Socket, payload: MessageRequestDto) { const { pinCode, message, position } = payload; diff --git a/packages/server/src/module/guards/session.guard.ts b/packages/server/src/module/guards/session.guard.ts new file mode 100644 index 00000000..77688a31 --- /dev/null +++ b/packages/server/src/module/guards/session.guard.ts @@ -0,0 +1,29 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { RedisService } from '../../config/database/redis/redis.service'; +import { WsException } from '@nestjs/websockets'; + +@Injectable() +export class SessionGuard implements CanActivate { + constructor(private redisService: RedisService) {} + + async canActivate(context: ExecutionContext) { + const client = context.switchToWs().getClient(); + + const { sid } = client.handshake.auth; + + if (!sid) { + client.emit('error', { message: 'Session ID not exists in Cookie.' }); + return false; + } + + if ( + (await this.redisService.exists(`master_sid=${sid}`)) || + (await this.redisService.exists(`participant_sid=${sid}`)) + ) { + client.emit('error', { message: 'Session ID not exists in Redis.' }); + } + + return true; + } +} diff --git a/packages/server/src/module/quiz/quizzes/quiz.service.ts b/packages/server/src/module/quiz/quizzes/quiz.service.ts index f2a4a8fc..9736b3df 100644 --- a/packages/server/src/module/quiz/quizzes/quiz.service.ts +++ b/packages/server/src/module/quiz/quizzes/quiz.service.ts @@ -69,10 +69,6 @@ export class QuizService { async getAllClasses(): Promise { const classEntities = await this.classRepository.findAll(); - if (!classEntities || classEntities.length === 0) { - throw new NotFoundException(`No classes found`); - } - return classEntities.map((classEntity: Class) => GetClassResponseDto.fromEntity(classEntity)); } diff --git a/packages/server/src/module/quiz/quizzes/repositories/class.repository.ts b/packages/server/src/module/quiz/quizzes/repositories/class.repository.ts index 79c1bed9..d6e7d503 100644 --- a/packages/server/src/module/quiz/quizzes/repositories/class.repository.ts +++ b/packages/server/src/module/quiz/quizzes/repositories/class.repository.ts @@ -34,10 +34,15 @@ export class ClassRepository { choices: true, }, }, + order: { + quizzes: { + position: 'ASC', + choices: { + position: 'ASC', + }, + }, + }, }); - if (!result) { - throw new NotFoundException(`No classes found`); - } return result; } catch (error) { if (error instanceof NotFoundException) throw error; diff --git a/packages/shared/types/gameStatus.types.ts b/packages/shared/types/gameStatus.types.ts index 8e0ed841..32aa523f 100644 --- a/packages/shared/types/gameStatus.types.ts +++ b/packages/shared/types/gameStatus.types.ts @@ -1,6 +1,8 @@ export const GAMESTATUS_TYPES = { WAITING: 'WAITING', IN_PROGRESS: 'IN PROGRESS', + RANKING: 'RANKING', + LEADERBORAD: 'LEADERBORAD', END: 'END', } as const;