diff --git a/backend/chatServer/src/chat/chat.error.ts b/backend/chatServer/src/chat/chat.error.ts new file mode 100644 index 00000000..691b7ab2 --- /dev/null +++ b/backend/chatServer/src/chat/chat.error.ts @@ -0,0 +1,54 @@ +import { HttpStatus } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; + +class ChatException extends WsException { + statusCode: number; + constructor({ statusCode, message } : ChatError , public roomId?: string) { + super({ statusCode, message, roomId }); + this.statusCode = statusCode; + } + + getError(): object { + return { + statusCode: this.statusCode, + msg: this.message, + roomId: this.roomId || null, + }; + } +} + +interface ChatError { + statusCode: number; + message: string; +} + +const CHATTING_SOCKET_ERROR = { + ROOM_EMPTY: { + statusCode: HttpStatus.BAD_REQUEST, + message: '유저가 참여하고 있는 채팅방이 없습니다.' + }, + + ROOM_EXISTED: { + statusCode: HttpStatus.BAD_REQUEST, + message: '이미 존재하는 방입니다.' + }, + + INVALID_USER: { + statusCode: HttpStatus.UNAUTHORIZED, + message: '유효하지 않는 유저입니다.' + }, + + QUESTION_EMPTY: { + statusCode: HttpStatus.BAD_REQUEST, + message: '유효하지 않은 질문입니다.' + }, + + BAN_USER: { + statusCode: HttpStatus.FORBIDDEN, + message: '호스트에 의해 밴 당한 유저입니다.' + } + + +}; +export { CHATTING_SOCKET_ERROR, ChatException }; + diff --git a/backend/chatServer/src/chat/chat.gateway.ts b/backend/chatServer/src/chat/chat.gateway.ts index 7966bd42..de7ad5b1 100644 --- a/backend/chatServer/src/chat/chat.gateway.ts +++ b/backend/chatServer/src/chat/chat.gateway.ts @@ -6,21 +6,21 @@ import { OnGatewayConnection, OnGatewayDisconnect, MessageBody, - ConnectedSocket, + ConnectedSocket } from '@nestjs/websockets'; import { UseGuards } from '@nestjs/common'; import { Server, Socket } from 'socket.io'; import { - CHATTING_SOCKET_DEFAULT_EVENT, - CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_SEND_EVENT + CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_SEND_EVENT } from '../event/constants'; import { + BanUserIncomingMessageDto, NormalIncomingMessageDto, NoticeIncomingMessageDto, QuestionDoneIncomingMessageDto, QuestionIncomingMessageDto } from '../event/dto/IncomingMessage.dto'; import { JoiningRoomDto } from '../event/dto/JoiningRoom.dto'; import { RoomService } from '../room/room.service'; import { createAdapter } from '@socket.io/redis-adapter'; -import { HostGuard, MessageGuard } from './chat.guard'; +import { BlacklistGuard, HostGuard, MessageGuard } from './chat.guard'; import { LeavingRoomDto } from '../event/dto/LeavingRoom.dto'; import { NormalOutgoingMessageDto, @@ -28,6 +28,7 @@ import { QuestionOutgoingMessageDto } from '../event/dto/OutgoingMessage.dto'; import { QuestionDto } from '../event/dto/Question.dto'; +import { ChatException, CHATTING_SOCKET_ERROR } from './chat.error'; @WebSocketGateway({ cors: true }) export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @@ -69,6 +70,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa } // 특정 방에 참여하기 위한 메서드 + @UseGuards(BlacklistGuard) @SubscribeMessage(CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM) async handleJoinRoom(client: Socket, payload: JoiningRoomDto) { const { roomId, userId } = payload; @@ -93,7 +95,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa } // 방에 NORMAL 메시지를 보내기 위한 메서드 - @UseGuards(MessageGuard) + @UseGuards(MessageGuard, BlacklistGuard) @SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.NORMAL) async handleNormalMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: NormalIncomingMessageDto) { const { roomId, userId, msg } = payload; @@ -103,7 +105,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa ...user, msg, msgTime: new Date().toISOString(), - msgType: 'normal' + msgType: 'normal', + socketId: client.id }; console.log('Normal Message Come In: ', normalOutgoingMessage); const hostId = await this.roomService.getHostOfRoom(roomId); @@ -121,7 +124,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa } // 방에 QUESTION 메시지를 보내기 위한 메서드 - @UseGuards(MessageGuard) + @UseGuards(MessageGuard,BlacklistGuard) @SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.QUESTION) async handleQuestionMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: QuestionIncomingMessageDto) { const { roomId, msg } = payload; @@ -132,7 +135,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa msg, msgTime: new Date().toISOString(), msgType: 'question', - questionDone: false + questionDone: false, + socketId: client.id }; const question: QuestionOutgoingMessageDto = await this.roomService.addQuestion(roomId, questionWithoutId); @@ -150,8 +154,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa } // 방에 NOTICE 메시지를 보내기 위한 메서드 - @UseGuards(MessageGuard) - @UseGuards(HostGuard) + @UseGuards(MessageGuard, HostGuard) @SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.NOTICE) async handleNoticeMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: NoticeIncomingMessageDto) { const { roomId, msg } = payload; @@ -165,4 +168,20 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa }; this.server.to(roomId).emit(CHATTING_SOCKET_RECEIVE_EVENT.NOTICE, noticeOutgoingMessage); } + + @UseGuards(HostGuard) + @SubscribeMessage(CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER) + async handleBanUserMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: BanUserIncomingMessageDto) { + const { roomId, socketId } = payload; + const banUser = this.server.sockets.sockets.get(socketId); + const address = banUser?.handshake.address.replaceAll('::ffff:', ''); + + if(!address) throw new ChatException(CHATTING_SOCKET_ERROR.INVALID_USER); + + const forwarded = banUser?.handshake.headers.forwarded ?? address; + console.log('ban:', roomId, address, forwarded); + + await this.roomService.addUserToBlacklist(roomId, address, forwarded); + console.log(await this.roomService.getUserBlacklist(roomId, address)); + } } diff --git a/backend/chatServer/src/chat/chat.guard.ts b/backend/chatServer/src/chat/chat.guard.ts index 270defc3..021baac0 100644 --- a/backend/chatServer/src/chat/chat.guard.ts +++ b/backend/chatServer/src/chat/chat.guard.ts @@ -1,5 +1,8 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { RoomService } from '../room/room.service'; +import { Socket } from 'socket.io'; + +import { ChatException, CHATTING_SOCKET_ERROR } from './chat.error'; @Injectable() export class MessageGuard implements CanActivate { @@ -22,3 +25,30 @@ export class HostGuard implements CanActivate { return hostId === userId; } } + +@Injectable() +export class BlacklistGuard implements CanActivate { + constructor(private roomService: RoomService) {}; + async canActivate(context: ExecutionContext) { + const payload = context.switchToWs().getData(); + const { roomId } = payload; + + const client: Socket = context.switchToWs().getClient(); + const address = client.handshake.address.replaceAll('::ffff:', ''); + const forwarded = client.handshake.headers.forwarded?.split(',')[0] ?? address; + + const isValidUser = await this.whenJoinRoom(roomId, address, forwarded); + + if(!isValidUser) throw new ChatException(CHATTING_SOCKET_ERROR.BAN_USER, roomId); + return true; + } + + async whenJoinRoom(roomId: string, address: string, forwarded: string) { + console.log(roomId, address, forwarded); + const blacklistInRoom = await this.roomService.getUserBlacklist(roomId, address); + console.log(blacklistInRoom); + const isInBlacklistUser = blacklistInRoom.some((blackForwarded) => blackForwarded === forwarded); + console.log('blacklistInRoom:', isInBlacklistUser); + return !isInBlacklistUser; + } +} diff --git a/backend/chatServer/src/chat/chat.module.ts b/backend/chatServer/src/chat/chat.module.ts index 51629715..bd285e1f 100644 --- a/backend/chatServer/src/chat/chat.module.ts +++ b/backend/chatServer/src/chat/chat.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { ChatGateway } from './chat.gateway'; import { RoomModule } from '../room/room.module'; -import { MessageGuard } from './chat.guard'; +import { BlacklistGuard, HostGuard, MessageGuard } from './chat.guard'; @Module({ imports: [RoomModule], - providers: [ChatGateway, MessageGuard], + providers: [ChatGateway, MessageGuard, BlacklistGuard, HostGuard], }) export class ChatModule {} diff --git a/backend/chatServer/src/event/constants.ts b/backend/chatServer/src/event/constants.ts index 2ce8d053..ce8fd0c6 100644 --- a/backend/chatServer/src/event/constants.ts +++ b/backend/chatServer/src/event/constants.ts @@ -1,8 +1,7 @@ -import { HttpStatus } from '@nestjs/common'; - const CHATTING_SOCKET_DEFAULT_EVENT = { JOIN_ROOM: 'join_room', LEAVE_ROOM: 'leave_room', + BAN_USER: 'ban_user', }; const CHATTING_SOCKET_RECEIVE_EVENT = { @@ -20,28 +19,4 @@ const CHATTING_SOCKET_SEND_EVENT = { NOTICE: 'send_notice' }; -const CHATTING_SOCKET_ERROR = { - ROOM_EMPTY : { - statusCode: HttpStatus.BAD_REQUEST, - message: '유저가 참여하고 있는 채팅방이 없습니다.' - }, - - ROOM_EXISTED: { - statusCode: HttpStatus.BAD_REQUEST, - message: '이미 존재하는 방입니다.' - }, - - INVALID_USER: { - statusCode: HttpStatus.UNAUTHORIZED, - message: '유효하지 않는 유저입니다.' - }, - - QUESTION_EMPTY: { - statusCode: HttpStatus.BAD_REQUEST, - message: '유효하지 않은 질문입니다.' - }, - - -}; - -export { CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_SEND_EVENT, CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_ERROR}; +export { CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_SEND_EVENT, CHATTING_SOCKET_RECEIVE_EVENT}; diff --git a/backend/chatServer/src/event/dto/OutgoingMessage.dto.ts b/backend/chatServer/src/event/dto/OutgoingMessage.dto.ts index 8557a5ed..f77eac50 100644 --- a/backend/chatServer/src/event/dto/OutgoingMessage.dto.ts +++ b/backend/chatServer/src/event/dto/OutgoingMessage.dto.ts @@ -5,6 +5,7 @@ class DefaultOutgoingMessageDto { roomId: string = ''; nickname: string = ''; color: string = ''; + entryTime: string = ''; msgTime: string = new Date().toISOString(); } diff --git a/backend/chatServer/src/event/dto/Question.dto.ts b/backend/chatServer/src/event/dto/Question.dto.ts index 3a7c78bb..67ab8788 100644 --- a/backend/chatServer/src/event/dto/Question.dto.ts +++ b/backend/chatServer/src/event/dto/Question.dto.ts @@ -4,6 +4,7 @@ class QuestionDto { roomId: string = ''; nickname: string = ''; color: string = ''; + entryTime: string = ''; msg: string = ''; msgTime: string = new Date().toISOString(); msgType: OutgoingMessageType = 'question'; diff --git a/backend/chatServer/src/room/room.repository.ts b/backend/chatServer/src/room/room.repository.ts index 6bb90b68..3289d2d0 100644 --- a/backend/chatServer/src/room/room.repository.ts +++ b/backend/chatServer/src/room/room.repository.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; import { Cluster } from 'ioredis'; -import { CHATTING_SOCKET_ERROR } from '../event/constants'; -import { WsException } from '@nestjs/websockets'; import { QuestionDto } from '../event/dto/Question.dto'; +import { ChatException, CHATTING_SOCKET_ERROR } from '../chat/chat.error'; + +type FORWARDED = string; @Injectable() export class RoomRepository { @@ -10,6 +11,7 @@ export class RoomRepository { roomIdPrefix = 'room:'; questionPrefix = 'question'; questionIdPrefix = 'id'; + blacklistPrefix = 'blacklist'; injectClient(redisClient: Cluster){ this.redisClient = redisClient; @@ -27,6 +29,14 @@ export class RoomRepository { return `${this.getRoomStringWithPrefix(roomId)}-${this.questionPrefix}-${this.questionIdPrefix}`; } + private getUserBlacklistStringWithPrefix(address:string){ + return `${this.blacklistPrefix}:${address}`; + } + + private getUserBlacklistInRoomWithPrefix(roomId:string, address:string){ + return `${this.getRoomStringWithPrefix(roomId)}-${this.getUserBlacklistStringWithPrefix(address)}`; + } + private async lindex(key: string, index: number){ const result = await this.redisClient.lindex(key, index); if(result) return JSON.parse(result) as T; @@ -35,12 +45,12 @@ export class RoomRepository { private async lrange(key: string, start: number, end: number){ const result = await this.redisClient.lrange(key, start, end); - return result.map((r) => JSON.parse(r)) as T; + return result as T; } private async getData(key: string) { const result = await this.redisClient.get(key); - if(result) return JSON.parse(result) as T; + if(result) return typeof result === 'string' ? result as T : JSON.parse(result) as T; return undefined; } @@ -50,7 +60,7 @@ export class RoomRepository { async getHost(roomId: string) { const hostId = await this.redisClient.get(this.getRoomStringWithPrefix(roomId)); - if(!hostId) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if(!hostId) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return hostId; } @@ -72,7 +82,7 @@ export class RoomRepository { async markQuestionAsDone(roomId: string, questionId: number): Promise { const question = await this.getQuestion(roomId, questionId); - if(!question) throw new WsException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); + if(!question) throw new ChatException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); question.questionDone = true; this.redisClient.lset(this.getQuestionStringWithPrefix(roomId), questionId, JSON.stringify(question)); return question; @@ -88,7 +98,7 @@ export class RoomRepository { async getQuestion(roomId: string, questionId: number): Promise { const question = await this.lindex>(this.getQuestionStringWithPrefix(roomId), questionId); if(question) return {...question, questionId }; - throw new WsException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); + throw new ChatException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); } async getQuestionId(roomId: string) { @@ -102,4 +112,16 @@ export class RoomRepository { } + async getUserBlacklist(roomId: string, address: string): Promise { + const userBlacklist = await this.lrange(this.getUserBlacklistInRoomWithPrefix(roomId, address), 0, -1); + console.log('blacklist', userBlacklist); + if (!userBlacklist) return []; + return userBlacklist; + } + + async addUserBlacklistToRoom(roomId: string, address: string, forwarded: string){ + console.log(roomId, address, forwarded); + console.log(this.getUserBlacklistInRoomWithPrefix(roomId, address)); + return this.redisClient.rpush(this.getUserBlacklistInRoomWithPrefix(roomId, address), forwarded); + } } diff --git a/backend/chatServer/src/room/room.service.ts b/backend/chatServer/src/room/room.service.ts index 3246d45e..e72126ec 100644 --- a/backend/chatServer/src/room/room.service.ts +++ b/backend/chatServer/src/room/room.service.ts @@ -1,8 +1,6 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { Cluster, Redis } from 'ioredis'; import { createAdapter } from '@socket.io/redis-adapter'; -import { WsException } from '@nestjs/websockets'; -import { CHATTING_SOCKET_ERROR } from '../event/constants'; import { User } from './user.interface'; import { getRandomAdjective, getRandomBrightColor, getRandomNoun } from '../utils/random'; import { RoomRepository } from './room.repository'; @@ -12,6 +10,7 @@ import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; +import { ChatException, CHATTING_SOCKET_ERROR } from '../chat/chat.error'; // 현재 파일의 URL을 파일 경로로 변환 const __filename = fileURLToPath(import.meta.url); @@ -28,7 +27,8 @@ function createRandomNickname(){ function createRandomUserInstance(): User { return { nickname: createRandomNickname(), - color: getRandomBrightColor() + color: getRandomBrightColor(), + entryTime: new Date().toISOString() }; } @@ -85,13 +85,13 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { // 방 삭제 async deleteRoom(roomId: string) { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); await this.redisRepository.deleteRoom(roomId); } async addQuestion(roomId: string, question: Omit){ const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return await this.redisRepository.addQuestionToRoom(roomId, question); } @@ -102,7 +102,7 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { question: Omit, ): Promise { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return await this.redisRepository.addQuestionToRoom(roomId, question); } @@ -110,24 +110,24 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { // 특정 질문 완료 처리 async markQuestionAsDone(roomId: string, questionId: number) { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); const markedQuestion = await this.redisRepository.markQuestionAsDone(roomId, questionId); - if (!markedQuestion) throw new WsException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); + if (!markedQuestion) throw new ChatException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY); return markedQuestion; } // 방에 속한 모든 질문 조회 async getQuestions(roomId: string): Promise { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return this.redisRepository.getQuestionsAll(roomId); } async getQuestionsNotDone(roomId: string): Promise { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return this.redisRepository.getQuestionsUnmarked(roomId); } @@ -135,7 +135,7 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { // 특정 질문 조회 async getQuestion(roomId: string, questionId: number): Promise { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return this.redisRepository.getQuestion(roomId, questionId); } @@ -149,7 +149,7 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { // 유저 삭제 async deleteUser(clientId: string) { const user = this.users.get(clientId); - if (!user) throw new WsException(CHATTING_SOCKET_ERROR.INVALID_USER); + if (!user) throw new ChatException(CHATTING_SOCKET_ERROR.INVALID_USER); this.users.delete(clientId); return user; } @@ -157,13 +157,24 @@ export class RoomService implements OnModuleInit, OnModuleDestroy { // 특정 유저 조회 async getUserByClientId(clientId: string) { const user = this.users.get(clientId); - if (!user) throw new WsException(CHATTING_SOCKET_ERROR.INVALID_USER); + if (!user) throw new ChatException(CHATTING_SOCKET_ERROR.INVALID_USER); return user; } async getHostOfRoom(roomId: string) { const roomExists = await this.redisRepository.isRoomExisted(roomId); - if (!roomExists) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); + if (!roomExists) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY); return await this.redisRepository.getHost(roomId); } + + async getUserBlacklist(roomId: string, address: string) { + const roomExists = await this.redisRepository.isRoomExisted(roomId); + if (!roomExists) return []; + + return await this.redisRepository.getUserBlacklist(roomId, address); + } + + async addUserToBlacklist(roomId: string, address: string, forwarded: string){ + return await this.redisRepository.addUserBlacklistToRoom(roomId, address, forwarded); + } } diff --git a/backend/chatServer/src/room/user.interface.ts b/backend/chatServer/src/room/user.interface.ts index a37d58a8..cba40228 100644 --- a/backend/chatServer/src/room/user.interface.ts +++ b/backend/chatServer/src/room/user.interface.ts @@ -1,4 +1,5 @@ interface User { + entryTime: string; nickname: string; color: string; }