Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
5 changes: 2 additions & 3 deletions src/auth/domain/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 16 additions & 21 deletions src/auth/infrastructure/guard/ws-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();

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;
Expand All @@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions src/cardset/infrastructure/grpc/user-grpc.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 };
}
}
10 changes: 10 additions & 0 deletions src/collaboration/application/collaboration.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -13,8 +14,17 @@ export class CollaborationUseCase {
private readonly yjsDocumentService: YjsDocumentService,
@InjectRepository(CardsetContentOrmEntity)
private readonly cardsetContentRepository: Repository<CardsetContentOrmEntity>,
@InjectRepository(CardsetManagerOrmEntity)
private readonly cardsetManagerRepository: Repository<CardsetManagerOrmEntity>,
) {}

async isManager(cardSetId: number, userId: number): Promise<boolean> {
const manager = await this.cardsetManagerRepository.findOne({
where: { cardSetId, userId },
});
return !!manager;
}

async getOrCreateDocument(cardsetId: number): Promise<Y.Doc> {
const fromRedis = await this.yjsDocumentService.loadDocument(
cardsetId.toString(),
Expand Down
7 changes: 6 additions & 1 deletion src/collaboration/collaboration.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand All @@ -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) {
Expand Down
28 changes: 26 additions & 2 deletions src/proto/user.proto
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
}
3 changes: 1 addition & 2 deletions src/shared/types/user-auth.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export interface UserAuth {
userId: string;
role: string;
tokenVersion: number;
nickname: string;
}
Loading