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/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/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/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 1d70afb..e1dc073 100644 --- a/src/collaboration/collaboration.module.ts +++ b/src/collaboration/collaboration.module.ts @@ -3,20 +3,25 @@ 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'; 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: [ TypeOrmModule.forFeature([ CardsetContentOrmEntity, CardsetIncrementalOrmEntity, + CardsetManagerOrmEntity, ]), AuthModule, + GrpcClientModule, ], - providers: [YjsDocumentService, CollaborationUseCase, CollaborationGateway], + providers: [YjsDocumentService, CollaborationUseCase, CollaborationGateway, UserGrpcClient], exports: [CollaborationUseCase], }) export class CollaborationModule {} diff --git a/src/collaboration/infrastructure/gateway/collaboration.gateway.ts b/src/collaboration/infrastructure/gateway/collaboration.gateway.ts index e89b22b..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,9 +93,14 @@ 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.joiningClients.delete(client.id); - this.logger.log(`User ${user.userId} joined cardset ${cardsetId}`); + this.logger.log(`User ${user.userId} (${user.nickname}) joined cardset ${cardsetId}`); const buffered = this.pendingUpdates.get(client.id); if (buffered && buffered.length > 0) { 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; }