From 5262de427428785ae13db71078577fad31bf2f75 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Tue, 12 Nov 2024 22:27:22 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20feat:=20chat=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/chat/domain/chat.entity.ts | 29 +++++++++++++++++++ .../backend/src/chat/domain/chatType.enum.ts | 6 ++++ 2 files changed, 35 insertions(+) create mode 100644 packages/backend/src/chat/domain/chat.entity.ts create mode 100644 packages/backend/src/chat/domain/chatType.enum.ts diff --git a/packages/backend/src/chat/domain/chat.entity.ts b/packages/backend/src/chat/domain/chat.entity.ts new file mode 100644 index 00000000..e855f196 --- /dev/null +++ b/packages/backend/src/chat/domain/chat.entity.ts @@ -0,0 +1,29 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { ChatType } from '@/chat/domain/chatType.enum'; +import { DateEmbedded } from '@/common/dateEmbedded.entity'; +import { Stock } from '@/stock/domain/stock.entity'; +import { User } from '@/user/domain/user.entity'; + +@Entity() +export class Chat { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, (user) => user.id) + user: User; + + @ManyToOne(() => Stock, (stock) => stock.id) + stock: Stock; + + @Column() + message: string; + + @Column() + type: ChatType; + + @Column({ name: 'like_count' }) + likeCount: number; + + @Column(() => DateEmbedded, { prefix: '' }) + date?: DateEmbedded; +} diff --git a/packages/backend/src/chat/domain/chatType.enum.ts b/packages/backend/src/chat/domain/chatType.enum.ts new file mode 100644 index 00000000..c078a2e9 --- /dev/null +++ b/packages/backend/src/chat/domain/chatType.enum.ts @@ -0,0 +1,6 @@ +export const ChatType = { + NORMAL: 'NORMAL', + BROADCAST: 'BROADCAST', +}; + +export type ChatType = (typeof ChatType)[keyof typeof ChatType]; From 7013b9f98d2a392a2c9c95efee04101e21d0fdcf Mon Sep 17 00:00:00 2001 From: kimminsu Date: Tue, 12 Nov 2024 22:36:13 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EA=B3=BC=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/app.module.ts | 4 +- packages/backend/src/chat/chat.gateway.ts | 45 +++++++++++++++++++ packages/backend/src/chat/chat.module.ts | 10 +++++ .../backend/src/configs/devTypeormConfig.ts | 2 +- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/chat/chat.gateway.ts create mode 100644 packages/backend/src/chat/chat.module.ts diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index c7c4b36b..c425d9e5 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -4,11 +4,12 @@ import { WinstonModule } from 'nest-winston'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from '@/auth/auth.module'; -import { logger } from '@/configs/logger.config'; +import { ChatModule } from '@/chat/chat.module'; import { typeormDevelopConfig, typeormProductConfig, } from '@/configs/devTypeormConfig'; +import { logger } from '@/configs/logger.config'; import { StockModule } from '@/stock/stock.module'; import { UserModule } from '@/user/user.module'; @@ -23,6 +24,7 @@ import { UserModule } from '@/user/user.module'; ), WinstonModule.forRoot(logger), AuthModule, + ChatModule, ], controllers: [AppController], providers: [AppService], diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts new file mode 100644 index 00000000..4237257c --- /dev/null +++ b/packages/backend/src/chat/chat.gateway.ts @@ -0,0 +1,45 @@ +import { Inject } from '@nestjs/common'; +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { Logger } from 'winston'; + +interface chatMessage { + room: string; + content: string; +} + +@WebSocketGateway({ namespace: 'chat' }) +export class ChatGateway implements OnGatewayConnection { + @WebSocketServer() + server: Server; + constructor(@Inject('winston') private readonly logger: Logger) {} + + @SubscribeMessage('chat') + handleConnectStock( + @MessageBody() message: chatMessage, + @ConnectedSocket() client: Socket, + ) { + const { room, content } = message; + if (!client.rooms.has(room)) { + client.emit('error', 'You are not in the room'); + this.logger.warn(`client is not in the room ${room}`); + return; + } + this.server.to(room).emit('chat', content); + } + + handleConnection(client: Socket) { + const room = client.handshake.query.stockId; + if (room) { + client.join(room); + this.logger.info(`client joined room ${room}`); + } + } +} diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts new file mode 100644 index 00000000..97dee7a2 --- /dev/null +++ b/packages/backend/src/chat/chat.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ChatGateway } from '@/chat/chat.gateway'; +import { Chat } from '@/chat/domain/chat.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Chat])], + providers: [ChatGateway], +}) +export class ChatModule {} diff --git a/packages/backend/src/configs/devTypeormConfig.ts b/packages/backend/src/configs/devTypeormConfig.ts index 0672e4a6..41ceff73 100644 --- a/packages/backend/src/configs/devTypeormConfig.ts +++ b/packages/backend/src/configs/devTypeormConfig.ts @@ -22,4 +22,4 @@ export const typeormDevelopConfig: TypeOrmModuleOptions = { database: process.env.DB_NAME, entities: [__dirname + '/../**/*.entity.{js,ts}'], logging: true, -}; \ No newline at end of file +}; From 840154c3b6f72b6a7d5f702c6202a7fbb2e5f17f Mon Sep 17 00:00:00 2001 From: kimminsu Date: Tue, 12 Nov 2024 23:18:53 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=A8=20feat:=20websocket=20exception?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.gateway.ts | 8 ++++--- .../filter/webSocketException.filter.ts | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/middlewares/filter/webSocketException.filter.ts diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index 4237257c..9190ba4a 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -1,4 +1,4 @@ -import { Inject } from '@nestjs/common'; +import { Inject, UseFilters } from '@nestjs/common'; import { ConnectedSocket, MessageBody, @@ -9,6 +9,7 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger } from 'winston'; +import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter'; interface chatMessage { room: string; @@ -16,13 +17,14 @@ interface chatMessage { } @WebSocketGateway({ namespace: 'chat' }) +@UseFilters(WebSocketExceptionFilter) export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server: Server; constructor(@Inject('winston') private readonly logger: Logger) {} @SubscribeMessage('chat') - handleConnectStock( + async handleConnectStock( @MessageBody() message: chatMessage, @ConnectedSocket() client: Socket, ) { @@ -35,7 +37,7 @@ export class ChatGateway implements OnGatewayConnection { this.server.to(room).emit('chat', content); } - handleConnection(client: Socket) { + async handleConnection(client: Socket) { const room = client.handshake.query.stockId; if (room) { client.join(room); diff --git a/packages/backend/src/middlewares/filter/webSocketException.filter.ts b/packages/backend/src/middlewares/filter/webSocketException.filter.ts new file mode 100644 index 00000000..9a2084d3 --- /dev/null +++ b/packages/backend/src/middlewares/filter/webSocketException.filter.ts @@ -0,0 +1,24 @@ +import { + ArgumentsHost, + Catch, + Inject, + WsExceptionFilter, +} from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; +import { Socket } from 'socket.io'; +import { Logger } from 'winston'; + +@Catch(WsException) +export class WebSocketExceptionFilter implements WsExceptionFilter { + constructor(@Inject('winston') private readonly logger: Logger) {} + catch(exception: WsException, host: ArgumentsHost) { + const client = host.switchToWs().getClient(); + const data = host.switchToWs().getData(); + const errorMessage = exception.message; + client.emit('error', { + message: errorMessage, + data, + }); + this.logger.error(`error occurred: ${errorMessage}`); + } +} From 88715d732f7614157f35deaf8ca7691b192b4f3a Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 10:17:35 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=EB=90=9C=20=EC=A2=85=EB=AA=A9=20ID=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=9E=85=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/app.module.ts | 6 ++---- packages/backend/src/chat/chat.gateway.ts | 12 +++++++++++- packages/backend/src/chat/chat.module.ts | 3 ++- packages/backend/src/stock/stock.module.ts | 1 + packages/backend/src/stock/stock.service.ts | 7 ++++++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index c425d9e5..2b10bcbe 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WinstonModule } from 'nest-winston'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { AuthModule } from '@/auth/auth.module'; import { ChatModule } from '@/chat/chat.module'; import { @@ -26,7 +24,7 @@ import { UserModule } from '@/user/user.module'; AuthModule, ChatModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index 9190ba4a..7ea49fba 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger } from 'winston'; import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter'; +import { StockService } from '@/stock/stock.service'; interface chatMessage { room: string; @@ -21,7 +22,10 @@ interface chatMessage { export class ChatGateway implements OnGatewayConnection { @WebSocketServer() server: Server; - constructor(@Inject('winston') private readonly logger: Logger) {} + constructor( + @Inject('winston') private readonly logger: Logger, + private readonly stockService: StockService, + ) {} @SubscribeMessage('chat') async handleConnectStock( @@ -39,6 +43,12 @@ export class ChatGateway implements OnGatewayConnection { async handleConnection(client: Socket) { const room = client.handshake.query.stockId; + if (!room || !(await this.stockService.checkStockExist(room as string))) { + client.emit('error', 'Invalid stockId'); + this.logger.warn(`client connected with invalid stockId: ${room}`); + client.disconnect(); + return; + } if (room) { client.join(room); this.logger.info(`client joined room ${room}`); diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index 97dee7a2..c057caf2 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ChatGateway } from '@/chat/chat.gateway'; import { Chat } from '@/chat/domain/chat.entity'; +import { StockModule } from '@/stock/stock.module'; @Module({ - imports: [TypeOrmModule.forFeature([Chat])], + imports: [TypeOrmModule.forFeature([Chat]), StockModule], providers: [ChatGateway], }) export class ChatModule {} diff --git a/packages/backend/src/stock/stock.module.ts b/packages/backend/src/stock/stock.module.ts index 9bbfd915..86628543 100644 --- a/packages/backend/src/stock/stock.module.ts +++ b/packages/backend/src/stock/stock.module.ts @@ -8,5 +8,6 @@ import { StockService } from './stock.service'; imports: [TypeOrmModule.forFeature([Stock])], controllers: [StockController], providers: [StockService], + exports: [StockService], }) export class StockModule {} diff --git a/packages/backend/src/stock/stock.service.ts b/packages/backend/src/stock/stock.service.ts index cca8f692..a139849c 100644 --- a/packages/backend/src/stock/stock.service.ts +++ b/packages/backend/src/stock/stock.service.ts @@ -22,7 +22,6 @@ export class StockService { }); } - // 유저 존재는 인증에서 확인가능해서 생략 async createUserStock(userId: number, stockId: string) { return await this.datasource.transaction(async (manager) => { await this.validateStockExists(stockId, manager); @@ -34,6 +33,12 @@ export class StockService { }); } + async checkStockExist(stockId: string) { + return await this.datasource.manager.exists(Stock, { + where: { id: stockId }, + }); + } + async deleteUserStock(userId: number, userStockId: number) { await this.datasource.transaction(async (manager) => { const userStock = await manager.findOne(UserStock, { From 61be94df278ef836a26d74eb2ed714b5cddb40dc Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 10:39:18 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20google?= =?UTF-8?q?=20oauth=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/auth/auth.module.ts | 6 +++--- .../src/auth/{ => google}/googleAuth.controller.ts | 10 +++++----- .../src/auth/{ => google}/googleAuth.service.spec.ts | 4 ++-- .../src/auth/{ => google}/googleAuth.service.ts | 2 +- .../src/auth/{ => google}/guard/google.guard.ts | 2 +- .../{passport => google/strategy}/google.strategy.ts | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) rename packages/backend/src/auth/{ => google}/googleAuth.controller.ts (79%) rename packages/backend/src/auth/{ => google}/googleAuth.service.spec.ts (95%) rename packages/backend/src/auth/{ => google}/googleAuth.service.ts (93%) rename packages/backend/src/auth/{ => google}/guard/google.guard.ts (99%) rename packages/backend/src/auth/{passport => google/strategy}/google.strategy.ts (95%) diff --git a/packages/backend/src/auth/auth.module.ts b/packages/backend/src/auth/auth.module.ts index ae2d0647..b6b0205c 100644 --- a/packages/backend/src/auth/auth.module.ts +++ b/packages/backend/src/auth/auth.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { GoogleAuthController } from '@/auth/googleAuth.controller'; -import { GoogleAuthService } from '@/auth/googleAuth.service'; -import { GoogleStrategy } from '@/auth/passport/google.strategy'; +import { GoogleAuthController } from '@/auth/google/googleAuth.controller'; +import { GoogleAuthService } from '@/auth/google/googleAuth.service'; +import { GoogleStrategy } from '@/auth/google/strategy/google.strategy'; import { SessionSerializer } from '@/auth/passport/session.serializer'; import { UserModule } from '@/user/user.module'; diff --git a/packages/backend/src/auth/googleAuth.controller.ts b/packages/backend/src/auth/google/googleAuth.controller.ts similarity index 79% rename from packages/backend/src/auth/googleAuth.controller.ts rename to packages/backend/src/auth/google/googleAuth.controller.ts index 1e159782..26fa4f36 100644 --- a/packages/backend/src/auth/googleAuth.controller.ts +++ b/packages/backend/src/auth/google/googleAuth.controller.ts @@ -1,8 +1,7 @@ import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; -import { GoogleAuthGuard } from '@/auth/guard/google.guard'; -import { User } from '@/user/domain/user.entity'; +import { GoogleAuthGuard } from '@/auth/google/guard/google.guard'; @ApiTags('Auth') @Controller('auth/google') @@ -21,9 +20,10 @@ export class GoogleAuthController { @Get('/redirect') @UseGuards(GoogleAuthGuard) - async handleRedirect(@Req() request: Request) { - const user = request.user as User; - return { nickname: user.nickname, email: user.email }; + async handleRedirect() { + return { + message: 'success google login', + }; } @ApiOperation({ diff --git a/packages/backend/src/auth/googleAuth.service.spec.ts b/packages/backend/src/auth/google/googleAuth.service.spec.ts similarity index 95% rename from packages/backend/src/auth/googleAuth.service.spec.ts rename to packages/backend/src/auth/google/googleAuth.service.spec.ts index 9b90c9e1..3d3f49be 100644 --- a/packages/backend/src/auth/googleAuth.service.spec.ts +++ b/packages/backend/src/auth/google/googleAuth.service.spec.ts @@ -1,5 +1,5 @@ -import { GoogleAuthService } from '@/auth/googleAuth.service'; -import { OauthUserInfo } from '@/auth/passport/google.strategy'; +import { GoogleAuthService } from '@/auth/google/googleAuth.service'; +import { OauthUserInfo } from '@/auth/google/strategy/google.strategy'; import { OauthType } from '@/user/domain/ouathType'; import { Role } from '@/user/domain/role'; import { User } from '@/user/domain/user.entity'; diff --git a/packages/backend/src/auth/googleAuth.service.ts b/packages/backend/src/auth/google/googleAuth.service.ts similarity index 93% rename from packages/backend/src/auth/googleAuth.service.ts rename to packages/backend/src/auth/google/googleAuth.service.ts index d19be403..cd80f18f 100644 --- a/packages/backend/src/auth/googleAuth.service.ts +++ b/packages/backend/src/auth/google/googleAuth.service.ts @@ -1,5 +1,5 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { OauthUserInfo } from '@/auth/passport/google.strategy'; +import { OauthUserInfo } from '@/auth/google/strategy/google.strategy'; import { UserService } from '@/user/user.service'; @Injectable() diff --git a/packages/backend/src/auth/guard/google.guard.ts b/packages/backend/src/auth/google/guard/google.guard.ts similarity index 99% rename from packages/backend/src/auth/guard/google.guard.ts rename to packages/backend/src/auth/google/guard/google.guard.ts index bfb85f36..58b47875 100644 --- a/packages/backend/src/auth/guard/google.guard.ts +++ b/packages/backend/src/auth/google/guard/google.guard.ts @@ -12,4 +12,4 @@ export class GoogleAuthGuard extends AuthGuard('google') { await super.logIn(request); return isActivate; } -} \ No newline at end of file +} diff --git a/packages/backend/src/auth/passport/google.strategy.ts b/packages/backend/src/auth/google/strategy/google.strategy.ts similarity index 95% rename from packages/backend/src/auth/passport/google.strategy.ts rename to packages/backend/src/auth/google/strategy/google.strategy.ts index 230ece55..28295a2d 100644 --- a/packages/backend/src/auth/passport/google.strategy.ts +++ b/packages/backend/src/auth/google/strategy/google.strategy.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy, VerifyCallback } from 'passport-google-oauth20'; -import { GoogleAuthService } from '@/auth/googleAuth.service'; +import { GoogleAuthService } from '@/auth/google/googleAuth.service'; import { OauthType } from '@/user/domain/ouathType'; export interface OauthUserInfo { From 433b2cc38793bc07c8266d8891ecfa5a7517fd8f Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 16:37:20 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=EC=9A=A9=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=20=EC=84=B8=EC=85=98=20=EC=A0=80=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/app.module.ts | 2 ++ packages/backend/src/auth/auth.module.ts | 2 +- packages/backend/src/auth/session.module.ts | 17 +++++++++++++++++ .../{passport => session}/session.serializer.ts | 0 packages/backend/src/main.ts | 4 +++- 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/auth/session.module.ts rename packages/backend/src/auth/{passport => session}/session.serializer.ts (100%) diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index 2b10bcbe..41ec9d85 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WinstonModule } from 'nest-winston'; import { AuthModule } from '@/auth/auth.module'; +import { SessionModule } from '@/auth/session.module'; import { ChatModule } from '@/chat/chat.module'; import { typeormDevelopConfig, @@ -23,6 +24,7 @@ import { UserModule } from '@/user/user.module'; WinstonModule.forRoot(logger), AuthModule, ChatModule, + SessionModule, ], controllers: [], providers: [], diff --git a/packages/backend/src/auth/auth.module.ts b/packages/backend/src/auth/auth.module.ts index b6b0205c..f513d613 100644 --- a/packages/backend/src/auth/auth.module.ts +++ b/packages/backend/src/auth/auth.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { GoogleAuthController } from '@/auth/google/googleAuth.controller'; import { GoogleAuthService } from '@/auth/google/googleAuth.service'; import { GoogleStrategy } from '@/auth/google/strategy/google.strategy'; -import { SessionSerializer } from '@/auth/passport/session.serializer'; +import { SessionSerializer } from '@/auth/session/session.serializer'; import { UserModule } from '@/user/user.module'; @Module({ diff --git a/packages/backend/src/auth/session.module.ts b/packages/backend/src/auth/session.module.ts new file mode 100644 index 00000000..07c36bba --- /dev/null +++ b/packages/backend/src/auth/session.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { MemoryStore } from 'express-session'; + +export const MEMORY_STORE = 'memoryStore'; + +@Module({ + providers: [ + { + provide: MEMORY_STORE, + useFactory: () => { + return new MemoryStore(); + }, + }, + ], + exports: [MEMORY_STORE], +}) +export class SessionModule {} diff --git a/packages/backend/src/auth/passport/session.serializer.ts b/packages/backend/src/auth/session/session.serializer.ts similarity index 100% rename from packages/backend/src/auth/passport/session.serializer.ts rename to packages/backend/src/auth/session/session.serializer.ts diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index 1012be4f..311bcdb9 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -3,12 +3,14 @@ import { NestFactory } from '@nestjs/core'; import * as session from 'express-session'; import * as passport from 'passport'; import { AppModule } from './app.module'; +import { MEMORY_STORE } from '@/auth/session.module'; import { sessionConfig } from '@/configs/session.config'; import { useSwagger } from '@/configs/swagger.config'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.use(session(sessionConfig)); + const store = app.get(MEMORY_STORE); + app.use(session({ ...sessionConfig, store })); app.useGlobalPipes(new ValidationPipe({ transform: true })); useSwagger(app); app.use(passport.initialize()); From 1486131d60fdf8c129450126a309893141b6eefa Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 16:38:47 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=A0=84=EC=9A=A9=20=EC=BF=A0=ED=82=A4=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=EA=B0=80=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/auth/session/cookieParser.ts | 31 +++++++++++++ .../auth/session/webSocketSession.guard..ts | 45 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 packages/backend/src/auth/session/cookieParser.ts create mode 100644 packages/backend/src/auth/session/webSocketSession.guard..ts diff --git a/packages/backend/src/auth/session/cookieParser.ts b/packages/backend/src/auth/session/cookieParser.ts new file mode 100644 index 00000000..2436b67a --- /dev/null +++ b/packages/backend/src/auth/session/cookieParser.ts @@ -0,0 +1,31 @@ +import * as crypto from 'node:crypto'; +import { WsException } from '@nestjs/websockets'; +import * as cookie from 'cookie'; +import { Socket } from 'socket.io'; +import { sessionConfig } from '@/configs/session.config'; + +export const websocketCookieParse = (socket: Socket) => { + if (!socket.request.headers.cookie) { + throw new WsException('not found cookie'); + } + const cookies = cookie.parse(socket.request.headers.cookie); + const sid = cookies['connect.sid']; + return getSessionIdFromCookie(sid); +}; + +const getSessionIdFromCookie = (cookieValue: string) => { + if (cookieValue.startsWith('s:')) { + const [id, signature] = cookieValue.slice(2).split('.'); + const expectedSignature = crypto + .createHmac('sha256', sessionConfig.secret) + .update(id) + .digest('base64') + .replace(/=+$/, ''); + + if (expectedSignature === signature) { + return id; + } + throw new WsException('Invalid cookie signature'); + } + throw new WsException('Invalid cookie format'); +}; diff --git a/packages/backend/src/auth/session/webSocketSession.guard..ts b/packages/backend/src/auth/session/webSocketSession.guard..ts new file mode 100644 index 00000000..a19ff956 --- /dev/null +++ b/packages/backend/src/auth/session/webSocketSession.guard..ts @@ -0,0 +1,45 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; +import { MemoryStore, SessionData } from 'express-session'; +import { Socket } from 'socket.io'; +import { websocketCookieParse } from '@/auth/session/cookieParser'; +import { MEMORY_STORE } from '@/auth/session.module'; +import { User } from '@/user/domain/user.entity'; + +export interface SessionSocket extends Socket { + session?: User; +} + +interface PassportSession extends SessionData { + passport: { user: User }; +} + +@Injectable() +export class WebSocketSessionGuard implements CanActivate { + constructor( + @Inject(MEMORY_STORE) private readonly sessionStore: MemoryStore, + ) {} + async canActivate(context: ExecutionContext): Promise { + const socket: SessionSocket = context.switchToHttp().getRequest(); + const cookieValue = websocketCookieParse(socket); + const session = await this.getSession(cookieValue); + socket.session = session.passport.user; + return true; + } + + private getSession(cookieValue: string) { + return new Promise((resolve, reject) => { + this.sessionStore.get(cookieValue, (err: Error, session) => { + if (err || !session) { + reject(new WsException('forbidden chat')); + } + resolve(session as PassportSession); + }); + }); + } +} From 11c88a96c776145dcb8369bce22febbc623b2e69 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 16:39:16 +0900 Subject: [PATCH 08/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/middlewares/filter/webSocketException.filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/middlewares/filter/webSocketException.filter.ts b/packages/backend/src/middlewares/filter/webSocketException.filter.ts index 9a2084d3..b1662fcb 100644 --- a/packages/backend/src/middlewares/filter/webSocketException.filter.ts +++ b/packages/backend/src/middlewares/filter/webSocketException.filter.ts @@ -19,6 +19,6 @@ export class WebSocketExceptionFilter implements WsExceptionFilter { message: errorMessage, data, }); - this.logger.error(`error occurred: ${errorMessage}`); + this.logger.warn(`error occurred: ${errorMessage}`); } } From 75369651744871536bb652de28b2bc08d9bc6817 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 16:44:06 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EB=90=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=A7=8C=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EC=9D=84=20=EC=9E=85=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.gateway.ts | 10 ++++++++-- packages/backend/src/chat/chat.module.ts | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index 7ea49fba..bbab4f8d 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -1,4 +1,4 @@ -import { Inject, UseFilters } from '@nestjs/common'; +import { Inject, UseFilters, UseGuards } from '@nestjs/common'; import { ConnectedSocket, MessageBody, @@ -9,6 +9,10 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger } from 'winston'; +import { + SessionSocket, + WebSocketSessionGuard, +} from '@/auth/session/webSocketSession.guard.'; import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter'; import { StockService } from '@/stock/stock.service'; @@ -27,12 +31,14 @@ export class ChatGateway implements OnGatewayConnection { private readonly stockService: StockService, ) {} + @UseGuards(WebSocketSessionGuard) @SubscribeMessage('chat') async handleConnectStock( @MessageBody() message: chatMessage, - @ConnectedSocket() client: Socket, + @ConnectedSocket() client: SessionSocket, ) { const { room, content } = message; + this.logger.info(`message from ${client.session?.nickname}`); if (!client.rooms.has(room)) { client.emit('error', 'You are not in the room'); this.logger.warn(`client is not in the room ${room}`); diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index c057caf2..6b188d93 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { SessionModule } from '@/auth/session.module'; import { ChatGateway } from '@/chat/chat.gateway'; import { Chat } from '@/chat/domain/chat.entity'; import { StockModule } from '@/stock/stock.module'; @Module({ - imports: [TypeOrmModule.forFeature([Chat]), StockModule], + imports: [TypeOrmModule.forFeature([Chat]), StockModule, SessionModule], providers: [ChatGateway], }) export class ChatModule {} From 2ca7e0514e35b2e5fbe628d9673ed818dfcda414 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Wed, 13 Nov 2024 17:18:17 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/chat/chat.gateway.ts | 31 +++++++++++++++++-- packages/backend/src/chat/chat.module.ts | 3 +- packages/backend/src/chat/chat.service.ts | 21 +++++++++++++ .../backend/src/chat/domain/chat.entity.ts | 8 ++--- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/chat/chat.service.ts diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index bbab4f8d..73b867cd 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -15,12 +15,21 @@ import { } from '@/auth/session/webSocketSession.guard.'; import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter'; import { StockService } from '@/stock/stock.service'; +import { ChatService } from '@/chat/chat.service'; +import { Chat } from '@/chat/domain/chat.entity'; interface chatMessage { room: string; content: string; } +interface chatResponse { + likeCount: number; + message: string; + type: string; + createdAt: Date; +} + @WebSocketGateway({ namespace: 'chat' }) @UseFilters(WebSocketExceptionFilter) export class ChatGateway implements OnGatewayConnection { @@ -29,6 +38,7 @@ export class ChatGateway implements OnGatewayConnection { constructor( @Inject('winston') private readonly logger: Logger, private readonly stockService: StockService, + private readonly chatService: ChatService, ) {} @UseGuards(WebSocketSessionGuard) @@ -38,13 +48,21 @@ export class ChatGateway implements OnGatewayConnection { @ConnectedSocket() client: SessionSocket, ) { const { room, content } = message; - this.logger.info(`message from ${client.session?.nickname}`); if (!client.rooms.has(room)) { client.emit('error', 'You are not in the room'); this.logger.warn(`client is not in the room ${room}`); return; } - this.server.to(room).emit('chat', content); + if (!client.session || !client.session.id) { + client.emit('error', 'Invalid session'); + this.logger.warn('client session is invalid'); + return; + } + const savedChat = await this.chatService.saveChat(client.session.id, { + stockId: room, + message: content, + }); + this.server.to(room).emit('chat', this.toResponse(savedChat)); } async handleConnection(client: Socket) { @@ -60,4 +78,13 @@ export class ChatGateway implements OnGatewayConnection { this.logger.info(`client joined room ${room}`); } } + + private toResponse(chat: Chat): chatResponse { + return { + likeCount: chat.likeCount, + message: chat.message, + type: chat.type, + createdAt: chat.date?.createdAt || new Date(), + }; + } } diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index 6b188d93..345a0f76 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -4,9 +4,10 @@ import { SessionModule } from '@/auth/session.module'; import { ChatGateway } from '@/chat/chat.gateway'; import { Chat } from '@/chat/domain/chat.entity'; import { StockModule } from '@/stock/stock.module'; +import { ChatService } from '@/chat/chat.service'; @Module({ imports: [TypeOrmModule.forFeature([Chat]), StockModule, SessionModule], - providers: [ChatGateway], + providers: [ChatGateway, ChatService], }) export class ChatModule {} diff --git a/packages/backend/src/chat/chat.service.ts b/packages/backend/src/chat/chat.service.ts new file mode 100644 index 00000000..c6fabff2 --- /dev/null +++ b/packages/backend/src/chat/chat.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Chat } from '@/chat/domain/chat.entity'; + +interface ChatMessage { + message: string; + stockId: string; +} + +@Injectable() +export class ChatService { + constructor(private readonly dataSource: DataSource) {} + + async saveChat(userId: number, chatMessage: ChatMessage) { + return this.dataSource.manager.save(Chat, { + user: { id: userId }, + stock: { id: chatMessage.stockId }, + message: chatMessage.message, + }); + } +} \ No newline at end of file diff --git a/packages/backend/src/chat/domain/chat.entity.ts b/packages/backend/src/chat/domain/chat.entity.ts index e855f196..e937d898 100644 --- a/packages/backend/src/chat/domain/chat.entity.ts +++ b/packages/backend/src/chat/domain/chat.entity.ts @@ -18,11 +18,11 @@ export class Chat { @Column() message: string; - @Column() - type: ChatType; + @Column({ type: 'enum', enum: ChatType, default: ChatType.NORMAL }) + type: ChatType = ChatType.NORMAL; - @Column({ name: 'like_count' }) - likeCount: number; + @Column({ name: 'like_count', default: 0 }) + likeCount: number = 0; @Column(() => DateEmbedded, { prefix: '' }) date?: DateEmbedded; From 66618881c626052e4543e12a2ae651fd3f8878a0 Mon Sep 17 00:00:00 2001 From: kimminsu Date: Thu, 14 Nov 2024 23:10:37 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?oauth=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1=EA=B3=B5?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/auth/google/googleAuth.controller.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/auth/google/googleAuth.controller.ts b/packages/backend/src/auth/google/googleAuth.controller.ts index 26fa4f36..1d460748 100644 --- a/packages/backend/src/auth/google/googleAuth.controller.ts +++ b/packages/backend/src/auth/google/googleAuth.controller.ts @@ -1,6 +1,6 @@ -import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Request } from 'express'; +import { Request, Response } from 'express'; import { GoogleAuthGuard } from '@/auth/google/guard/google.guard'; @ApiTags('Auth') @@ -20,10 +20,8 @@ export class GoogleAuthController { @Get('/redirect') @UseGuards(GoogleAuthGuard) - async handleRedirect() { - return { - message: 'success google login', - }; + async handleRedirect(@Res() response: Response) { + response.redirect('/'); } @ApiOperation({