From d7ab1213ac13dfd713aaef83f1ee6963ce518ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9D=EB=B2=94?= Date: Wed, 1 Apr 2026 22:35:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=ED=98=91=EC=97=85=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.module.ts | 6 ++- .../infrastructure/guard/ws-auth.guard.ts | 37 ++++++++----------- .../infrastructure/grpc/user-grpc.client.ts | 13 +++++++ .../gateway/collaboration.gateway.ts | 9 ++++- src/proto/user.proto | 28 +++++++++++++- src/shared/types/user-auth.type.ts | 3 +- 6 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index db9b10d..dfc5788 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,11 +2,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from './domain/auth.service'; import { WsAuthGuard } from './infrastructure/guard/ws-auth.guard'; +import { UserGrpcClient } from '../cardset/infrastructure/grpc/user-grpc.client'; +import { GrpcClientModule } from '../shared/grpc/grpc-client.module'; import authConfig from '../shared/config/auth.config'; @Module({ - imports: [ConfigModule.forFeature(authConfig)], - providers: [AuthService, WsAuthGuard], + imports: [ConfigModule.forFeature(authConfig), GrpcClientModule], + providers: [AuthService, WsAuthGuard, UserGrpcClient], exports: [AuthService, WsAuthGuard], }) export class AuthModule {} diff --git a/src/auth/infrastructure/guard/ws-auth.guard.ts b/src/auth/infrastructure/guard/ws-auth.guard.ts index 948922f..c63322e 100644 --- a/src/auth/infrastructure/guard/ws-auth.guard.ts +++ b/src/auth/infrastructure/guard/ws-auth.guard.ts @@ -4,33 +4,23 @@ import { Injectable, Logger, } from '@nestjs/common'; -import { AuthService } from '../../domain/auth.service'; import { Socket } from 'socket.io'; +import { UserGrpcClient } from '../../../cardset/infrastructure/grpc/user-grpc.client'; @Injectable() export class WsAuthGuard implements CanActivate { private readonly logger = new Logger(WsAuthGuard.name); - constructor(private readonly authService: AuthService) { } + constructor(private readonly userGrpcClient: UserGrpcClient) {} - canActivate(context: ExecutionContext): boolean { + async canActivate(context: ExecutionContext): Promise { const client: Socket = context.switchToWs().getClient(); - const SKIP_AUTH = process.env.SKIP_WS_AUTH === 'true' || true; - if (SKIP_AUTH) { - (client.data as { user: unknown }).user = { - userId: 'test-user', - email: 'test@example.com', - }; - this.logger.warn( - `⚠️ 테스트 모드: 인증을 건너뛰고 있습니다 (client ${client.id})`, - ); - return true; - } - + const rawAuth: unknown = client.handshake.auth?.token; + const rawHeader = client.handshake.headers?.authorization; const bearer = - (client.handshake.auth?.token as string | undefined) ?? - client.handshake.headers?.authorization; + (typeof rawAuth === 'string' ? rawAuth : undefined) ?? + (typeof rawHeader === 'string' ? rawHeader : undefined); const token = bearer && bearer.startsWith('Bearer ') ? bearer.slice(7) : bearer; @@ -41,12 +31,17 @@ export class WsAuthGuard implements CanActivate { } try { - const user = this.authService.verify(token); - (client.data as { user: unknown }).user = user; + const { userId, nickname } = await this.userGrpcClient.getUserByToken(token); + (client.data as { user: unknown }).user = { + userId: String(userId), + nickname, + }; return true; - } catch (error) { + } catch (err: unknown) { this.logger.warn( - `Invalid token for client ${client.id}: ${error instanceof Error ? error.message : 'Unknown error'}`, + `Token verification failed for client ${client.id}: ${ + err instanceof Error ? err.message : String(err) + }`, ); return false; } diff --git a/src/cardset/infrastructure/grpc/user-grpc.client.ts b/src/cardset/infrastructure/grpc/user-grpc.client.ts index 0562d10..69f3fe1 100644 --- a/src/cardset/infrastructure/grpc/user-grpc.client.ts +++ b/src/cardset/infrastructure/grpc/user-grpc.client.ts @@ -11,6 +11,10 @@ export interface UserInfo { interface UserQueryService { getUsers(data: { userIds: number[] }): Observable<{ users: UserInfo[] }>; + getUserByToken(data: { access_token: string }): Observable<{ + user_id: number; + nickname: string; + }>; } @Injectable() @@ -31,4 +35,13 @@ export class UserGrpcClient implements OnModuleInit { const result = await firstValueFrom(this.userService.getUsers({ userIds })); return result.users; } + + async getUserByToken( + accessToken: string, + ): Promise<{ userId: number; nickname: string }> { + const result = await firstValueFrom( + this.userService.getUserByToken({ access_token: accessToken }), + ); + return { userId: result.user_id, nickname: result.nickname }; + } } diff --git a/src/collaboration/infrastructure/gateway/collaboration.gateway.ts b/src/collaboration/infrastructure/gateway/collaboration.gateway.ts index f5195d2..7bc2428 100644 --- a/src/collaboration/infrastructure/gateway/collaboration.gateway.ts +++ b/src/collaboration/infrastructure/gateway/collaboration.gateway.ts @@ -77,8 +77,15 @@ export class CollaborationGateway const state = Y.encodeStateAsUpdate(doc); client.emit('sync', { cardsetId, update: Array.from(state) }); + client.emit('joined', { + cardsetId, + userId: user.userId, + nickname: user.nickname, + }); - this.logger.log(`User ${user.userId} joined cardset ${cardsetId}`); + this.logger.log( + `User ${user.userId} (${user.nickname}) joined cardset ${cardsetId}`, + ); } catch (error) { this.logger.error('Error joining cardset:', error); this.logger.error('Error details:', { diff --git a/src/proto/user.proto b/src/proto/user.proto index e09bb79..ddf8327 100644 --- a/src/proto/user.proto +++ b/src/proto/user.proto @@ -1,10 +1,16 @@ syntax = "proto3"; +option java_package = "flipnote.user.grpc"; +option java_outer_classname = "UserQueryProto"; +option java_multiple_files = true; + package user_query; service UserQueryService { - rpc GetUser (GetUserRequest) returns (GetUserResponse); - rpc GetUsers (GetUsersRequest) returns (GetUsersResponse); + rpc GetUser(GetUserRequest) returns (GetUserResponse); + rpc GetUsers(GetUsersRequest) returns (GetUsersResponse); + rpc GetUserByEmail(GetUserByEmailRequest) returns (GetUserByEmailResponse); + rpc GetUserByToken(GetUserByTokenRequest) returns (GetUserByTokenResponse); } message GetUserRequest { @@ -25,3 +31,21 @@ message GetUsersRequest { message GetUsersResponse { repeated GetUserResponse users = 1; } + +message GetUserByEmailRequest { + string email = 1; +} + +message GetUserByEmailResponse { + bool exists = 1; + GetUserResponse user = 2; +} + +message GetUserByTokenRequest { + string access_token = 1; +} + +message GetUserByTokenResponse { + int64 user_id = 1; + string nickname = 2; +} \ No newline at end of file diff --git a/src/shared/types/user-auth.type.ts b/src/shared/types/user-auth.type.ts index bf82ae7..8514f35 100644 --- a/src/shared/types/user-auth.type.ts +++ b/src/shared/types/user-auth.type.ts @@ -1,5 +1,4 @@ export interface UserAuth { userId: string; - role: string; - tokenVersion: number; + nickname: string; } From eb2fbe5038f1ee3ea550426c8ee71d773c15d144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9D=EB=B2=94?= Date: Wed, 1 Apr 2026 22:40:53 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Fix:=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/domain/auth.service.ts | 5 ++--- src/collaboration/collaboration.module.ts | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/auth/domain/auth.service.ts b/src/auth/domain/auth.service.ts index 6adca29..386c635 100644 --- a/src/auth/domain/auth.service.ts +++ b/src/auth/domain/auth.service.ts @@ -24,12 +24,11 @@ export class AuthService { ) & JwtUser; - const { user_id, role, token_version } = payload; + const { user_id } = payload; return { userId: user_id, - role, - tokenVersion: token_version, + nickname: '', }; } catch (e) { throw new UnauthorizedException(e); diff --git a/src/collaboration/collaboration.module.ts b/src/collaboration/collaboration.module.ts index 1d70afb..bb2da1a 100644 --- a/src/collaboration/collaboration.module.ts +++ b/src/collaboration/collaboration.module.ts @@ -7,6 +7,8 @@ import { YjsDocumentService } from './infrastructure/redis/yjs-document.service' import { CollaborationUseCase } from './application/collaboration.use-case'; import { CollaborationGateway } from './infrastructure/gateway/collaboration.gateway'; import { AuthModule } from '../auth/auth.module'; +import { UserGrpcClient } from '../cardset/infrastructure/grpc/user-grpc.client'; +import { GrpcClientModule } from '../shared/grpc/grpc-client.module'; @Module({ imports: [ @@ -15,8 +17,9 @@ import { AuthModule } from '../auth/auth.module'; CardsetIncrementalOrmEntity, ]), AuthModule, + GrpcClientModule, ], - providers: [YjsDocumentService, CollaborationUseCase, CollaborationGateway], + providers: [YjsDocumentService, CollaborationUseCase, CollaborationGateway, UserGrpcClient], exports: [CollaborationUseCase], }) export class CollaborationModule {} From fa5766f4b7ca743d5f4ce552d3664c93ace23b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9D=EB=B2=94?= Date: Thu, 2 Apr 2026 11:33:19 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Fix:=20=ED=8E=B8=EC=A7=91=20=EC=9E=85?= =?UTF-8?q?=EC=9E=A5=EC=8B=9C=20=EB=A7=A4=EB=8B=88=EC=A0=80=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/collaboration.use-case.ts | 10 +++++++++ src/collaboration/collaboration.module.ts | 2 ++ .../gateway/collaboration.gateway.ts | 22 ++++++++++++++----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/collaboration/application/collaboration.use-case.ts b/src/collaboration/application/collaboration.use-case.ts index 5b6deaa..250a82e 100644 --- a/src/collaboration/application/collaboration.use-case.ts +++ b/src/collaboration/application/collaboration.use-case.ts @@ -4,6 +4,7 @@ import { Repository } from 'typeorm'; import * as Y from 'yjs'; import { YjsDocumentService } from '../infrastructure/redis/yjs-document.service'; import { CardsetContentOrmEntity } from '../infrastructure/persistence/orm/cardset-content.orm-entity'; +import { CardsetManagerOrmEntity } from '../../cardset/infrastructure/persistence/orm/cardset-manager.orm-entity'; @Injectable() export class CollaborationUseCase { @@ -13,8 +14,17 @@ export class CollaborationUseCase { private readonly yjsDocumentService: YjsDocumentService, @InjectRepository(CardsetContentOrmEntity) private readonly cardsetContentRepository: Repository, + @InjectRepository(CardsetManagerOrmEntity) + private readonly cardsetManagerRepository: Repository, ) {} + async isManager(cardSetId: number, userId: number): Promise { + const manager = await this.cardsetManagerRepository.findOne({ + where: { cardSetId, userId }, + }); + return !!manager; + } + async getOrCreateDocument(cardsetId: number): Promise { const fromRedis = await this.yjsDocumentService.loadDocument( cardsetId.toString(), diff --git a/src/collaboration/collaboration.module.ts b/src/collaboration/collaboration.module.ts index bb2da1a..e1dc073 100644 --- a/src/collaboration/collaboration.module.ts +++ b/src/collaboration/collaboration.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CardsetContentOrmEntity } from './infrastructure/persistence/orm/cardset-content.orm-entity'; import { CardsetIncrementalOrmEntity } from './infrastructure/persistence/orm/cardset-incremental.orm-entity'; +import { CardsetManagerOrmEntity } from '../cardset/infrastructure/persistence/orm/cardset-manager.orm-entity'; import { YjsDocumentService } from './infrastructure/redis/yjs-document.service'; import { CollaborationUseCase } from './application/collaboration.use-case'; import { CollaborationGateway } from './infrastructure/gateway/collaboration.gateway'; @@ -15,6 +16,7 @@ import { GrpcClientModule } from '../shared/grpc/grpc-client.module'; TypeOrmModule.forFeature([ CardsetContentOrmEntity, CardsetIncrementalOrmEntity, + CardsetManagerOrmEntity, ]), AuthModule, GrpcClientModule, diff --git a/src/collaboration/infrastructure/gateway/collaboration.gateway.ts b/src/collaboration/infrastructure/gateway/collaboration.gateway.ts index 2e2b7b0..0495065 100644 --- a/src/collaboration/infrastructure/gateway/collaboration.gateway.ts +++ b/src/collaboration/infrastructure/gateway/collaboration.gateway.ts @@ -61,6 +61,18 @@ export class CollaborationGateway ); try { + const isManager = await this.collaborationUseCase.isManager( + Number(cardsetId), + Number(user.userId), + ); + if (!isManager) { + this.logger.warn( + `[cardset 입장 거부] 매니저 아님 - userId=${user.userId}, cardsetId=${cardsetId}`, + ); + client.emit('error', { message: '카드셋 편집 권한이 없습니다.' }); + return; + } + this.joiningClients.add(client.id); void client.join(`cardset:${cardsetId}`); @@ -81,11 +93,11 @@ export class CollaborationGateway const state = Y.encodeStateAsUpdate(doc); client.emit('sync', { cardsetId, update: Array.from(state) }); - client.emit('joined', { - cardsetId, - userId: user.userId, - nickname: user.nickname - }); + // client.emit('joined', { + // cardsetId, + // userId: user.userId, + // nickname: user.nickname + // }); this.joiningClients.delete(client.id); this.logger.log(`User ${user.userId} (${user.nickname}) joined cardset ${cardsetId}`);