-
Notifications
You must be signed in to change notification settings - Fork 0
main 반영 및 deploy #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
main 반영 및 deploy #167
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ec13f68
refactor: npm ci & prisma
annalee8595 fa07f07
Merge branch 'dev' of https://github.com/UMC-Eum/Backend into dev
annalee8595 7526566
Merge branch 'dev' of https://github.com/UMC-Eum/Backend into dev
annalee8595 314155a
fix: swagger 예시 키워드 수정
annalee8595 9a243d4
Merge branch 'dev' into fix/swagger-keyword
annalee8595 fb75634
Merge branch 'feat/send-chat-media' of https://github.com/UMC-Eum/Bac…
jihuuuu e16bed8
feat: 차단 시 채팅 기능 제한(생성/전송/읽음/삭제) 및 WS 에러 응답 통일
jihuuuu f2dacee
Merge branch 'dev' of https://github.com/UMC-Eum/Backend into feat/bl…
jihuuuu d3220f3
chore: 마이그레이션 반영
jihuuuu 532a7f1
refactor: ws 공용 모듈 분리로 gateway 역할 축소
jihuuuu 4a1dc47
fix: WebSocket 예외 처리 로깅 보강
jihuuuu d38cc2a
fix: lint 수정
jihuuuu 9761b4c
Merge pull request #162 from UMC-Eum/feat/block-sending-message
jihuuuu 11c0216
fix: npm format
annalee8595 44ab46f
Merge branch 'fix/swagger-keyword' of https://github.com/UMC-Eum/Back…
annalee8595 8eef74c
Merge pull request #155 from UMC-Eum/fix/swagger-keyword
annalee8595 b6b726a
refactor : 서비스 레이어 파라미터 추가
Lidohyeon f7791d7
refactor : 마음 생성시 파라미터 추가
Lidohyeon 0ef9cbd
refactor : Get 마음 API vibevector 삭제
Lidohyeon e475408
refactor : block interceptor 로직 수정
Lidohyeon 6532331
Merge pull request #166 from UMC-Eum/165-refactor-알림-파라미터-수정-및-block-…
Lidohyeon 8a92b3a
Merge pull request #164 from UMC-Eum/feat/active-now
jihuuuu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { | ||
| ArgumentsHost, | ||
| Catch, | ||
| HttpException, | ||
| Logger, | ||
| type ExceptionFilter, | ||
| } from '@nestjs/common'; | ||
| import { WsException } from '@nestjs/websockets'; | ||
| import type { Socket } from 'socket.io'; | ||
|
|
||
| import { AppException } from '../errors/app.exception'; | ||
| import type { ExternalErrorCode } from '../errors/error-codes'; | ||
| import type { ApiFailResponse } from '../dto/api-response.dto'; | ||
|
|
||
| function isRecord(v: unknown): v is Record<string, unknown> { | ||
| return typeof v === 'object' && v !== null; | ||
| } | ||
|
|
||
| function toErrorInfo(e: unknown): { | ||
| name: string; | ||
| message: string; | ||
| stack?: string; | ||
| } { | ||
| if (e instanceof Error) { | ||
| return { name: e.name, message: e.message, stack: e.stack }; | ||
| } | ||
| return { name: 'UnknownError', message: String(e) }; | ||
| } | ||
|
|
||
| function safeJson(v: unknown): string { | ||
| try { | ||
| return JSON.stringify(v); | ||
| } catch { | ||
| return '[unserializable]'; | ||
| } | ||
| } | ||
|
|
||
| @Catch() | ||
| export class WsExceptionFilter implements ExceptionFilter { | ||
| private readonly logger = new Logger(WsExceptionFilter.name); | ||
|
|
||
| catch(exception: unknown, host: ArgumentsHost) { | ||
| const ws = host.switchToWs(); | ||
| const client = ws.getClient<Socket>(); | ||
|
|
||
| const timestamp = new Date().toISOString(); | ||
| const path = `ws${client?.nsp?.name ?? ''}`; | ||
|
|
||
| const socketId = client?.id ?? 'N/A'; | ||
| const ip = (client?.handshake as { address?: string } | undefined)?.address; | ||
|
|
||
| // Socket.data는 프로젝트에서 타입을 강하게 안 박았을 수 있어서 unknown 처리 | ||
| const dataUnknown = (client as unknown as { data?: unknown }).data; | ||
| const userId = | ||
| isRecord(dataUnknown) && typeof dataUnknown.userId === 'number' | ||
| ? dataUnknown.userId | ||
| : undefined; | ||
|
|
||
| let code: ExternalErrorCode = 'SYS-001'; | ||
| let message = '잠시 문제가 발생했어요. 잠시 후 다시 시도해 주세요.'; | ||
|
|
||
| if (exception instanceof AppException) { | ||
| const body = exception.getResponse(); | ||
|
|
||
| if (isRecord(body)) { | ||
| if (typeof body.code === 'string') { | ||
| code = body.code as ExternalErrorCode; | ||
| } | ||
| if (typeof body.message === 'string') { | ||
| message = body.message; | ||
| } | ||
| } else if (typeof body === 'string') { | ||
| message = body; | ||
| } | ||
|
|
||
| // 예상 가능한 도메인 예외: 운영 관점에서 원인 파악 가능하도록 warn 로깅 | ||
| this.logger.warn( | ||
| `ws app exception: code=${code} socket=${socketId} userId=${String(userId)} ip=${String(ip)} path=${path}`, | ||
| ); | ||
| } else if (exception instanceof WsException) { | ||
| const err = exception.getError(); | ||
|
|
||
| if (typeof err === 'string') { | ||
| message = err; | ||
| } else if (isRecord(err)) { | ||
| if (typeof err.code === 'string') { | ||
| code = err.code as ExternalErrorCode; | ||
| } | ||
| if (typeof err.message === 'string') { | ||
| message = err.message; | ||
| } | ||
| } | ||
|
|
||
| // 예상 가능한 WS 예외: warn 로깅 | ||
| this.logger.warn( | ||
| `ws exception: code=${code} socket=${socketId} userId=${String(userId)} ip=${String(ip)} path=${path} detail=${safeJson(err)}`, | ||
| ); | ||
| } else if (exception instanceof HttpException) { | ||
| const body = exception.getResponse(); | ||
|
|
||
| if (typeof body === 'string') { | ||
| message = body; | ||
| } else if (exception.message) { | ||
| message = exception.message; | ||
| } | ||
|
|
||
| // HTTP 예외가 WS로 흘러온 케이스: warn 로깅 | ||
| this.logger.warn( | ||
| `ws http exception: status=${exception.getStatus()} socket=${socketId} userId=${String(userId)} ip=${String(ip)} path=${path} detail=${safeJson(body)}`, | ||
| ); | ||
| } else if (exception instanceof Error) { | ||
| // 예상 못한 서버 에러: 반드시 error + stack 로깅 (피드백 해결 핵심) | ||
| const info = toErrorInfo(exception); | ||
| this.logger.error( | ||
| `ws unexpected error: socket=${socketId} userId=${String(userId)} ip=${String(ip)} path=${path} err=${info.name}:${info.message}`, | ||
| info.stack, | ||
| ); | ||
|
|
||
| if (process.env.NODE_ENV !== 'production' && exception.message) { | ||
| message = exception.message; | ||
| } | ||
| } else { | ||
| // unknown도 로깅 | ||
| const info = toErrorInfo(exception); | ||
| this.logger.error( | ||
| `ws unknown throw: socket=${socketId} userId=${String(userId)} ip=${String(ip)} path=${path} err=${info.name}:${info.message}`, | ||
| ); | ||
|
|
||
| if (process.env.NODE_ENV !== 'production') { | ||
| message = info.message; | ||
| } | ||
| } | ||
|
|
||
| const payload: ApiFailResponse = { | ||
| resultType: 'FAIL', | ||
| success: null, | ||
| error: { code, message }, | ||
| meta: { timestamp, path }, | ||
| }; | ||
|
|
||
| // socket.io 기반 NestWS는 기본적으로 'exception' 이벤트로 에러를 전달 | ||
| client?.emit('exception', payload); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import { Injectable, Logger } from '@nestjs/common'; | ||
| import { ConfigService } from '@nestjs/config'; | ||
| import { ActiveStatus } from '@prisma/client'; | ||
| import type { DefaultEventsMap, Socket } from 'socket.io'; | ||
|
|
||
| import { PrismaService } from '../../prisma/prisma.service'; | ||
| import { JwtTokenService } from '../../../modules/auth/services/jwt-token.service'; | ||
| import { toUserRoom } from '../utils/ws-rooms.util'; | ||
|
|
||
| type SocketData = { userId?: number }; | ||
|
|
||
| type AuthedSocket = Socket< | ||
| DefaultEventsMap, | ||
| DefaultEventsMap, | ||
| DefaultEventsMap, | ||
| SocketData | ||
| >; | ||
|
|
||
| function extractBearerToken(client: AuthedSocket): string | null { | ||
| const auth = client.handshake.auth as Record<string, unknown> | undefined; | ||
| const query = client.handshake.query as Record<string, unknown> | undefined; | ||
|
|
||
| const raw = | ||
| auth?.token ?? auth?.accessToken ?? query?.token ?? query?.accessToken; | ||
|
|
||
| if (typeof raw !== 'string') return null; | ||
|
|
||
| const trimmed = raw.trim(); | ||
| if (!trimmed) return null; | ||
|
|
||
| return trimmed.startsWith('Bearer ') ? trimmed : `Bearer ${trimmed}`; | ||
| } | ||
|
|
||
| function toErrorInfo(e: unknown): { | ||
| name: string; | ||
| message: string; | ||
| stack?: string; | ||
| } { | ||
| if (e instanceof Error) { | ||
| return { name: e.name, message: e.message, stack: e.stack }; | ||
| } | ||
| return { name: 'UnknownError', message: String(e) }; | ||
| } | ||
|
|
||
| function toPositiveBigInt(v: unknown): bigint | null { | ||
| if (typeof v === 'number') { | ||
| if (!Number.isFinite(v) || v <= 0) return null; | ||
| return BigInt(Math.floor(v)); | ||
| } | ||
|
|
||
| if (typeof v === 'string') { | ||
| const s = v.trim(); | ||
| if (!/^\d+$/.test(s)) return null; | ||
|
|
||
| try { | ||
| const n = BigInt(s); | ||
| if (n <= 0n) return null; | ||
| return n; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| @Injectable() | ||
| export class WsAuthService { | ||
| private readonly logger = new Logger(WsAuthService.name); | ||
|
|
||
| constructor( | ||
| private readonly prisma: PrismaService, | ||
| private readonly jwtTokenService: JwtTokenService, | ||
| private readonly configService: ConfigService, | ||
| ) {} | ||
|
|
||
| async attachUser(client: AuthedSocket): Promise<number | null> { | ||
| const socketId = client.id; | ||
| const ip = client.handshake.address; | ||
|
|
||
| const bearer = extractBearerToken(client); | ||
| if (!bearer) { | ||
| this.logger.debug( | ||
| `attachUser auth failed: missing token socket=${socketId} ip=${ip}`, | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| const token = bearer.replace(/^Bearer\s+/i, '').trim(); | ||
| if (!token) { | ||
| this.logger.debug( | ||
| `attachUser auth failed: empty token socket=${socketId} ip=${ip}`, | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| const secret = this.configService.get<string>( | ||
| 'JWT_ACCESS_SECRET', | ||
| 'dev-access-secret', | ||
| ); | ||
|
|
||
| let subId: bigint | null = null; | ||
|
|
||
| try { | ||
| const payload = this.jwtTokenService.verify(token, secret) as unknown; | ||
| const sub = (payload as { sub?: unknown }).sub; | ||
|
|
||
| subId = toPositiveBigInt(sub); | ||
| if (!subId) { | ||
| this.logger.warn( | ||
| `attachUser auth failed: invalid sub socket=${socketId} ip=${ip}`, | ||
| ); | ||
| return null; | ||
| } | ||
| } catch (e) { | ||
| const info = toErrorInfo(e); | ||
| // 토큰 원문은 절대 로그에 남기지 않음 | ||
| this.logger.warn( | ||
| `attachUser auth failed: jwt verify error socket=${socketId} ip=${ip} err=${info.name}:${info.message}`, | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| const userRecord = await this.prisma.user.findFirst({ | ||
| where: { | ||
| id: subId, | ||
| deletedAt: null, | ||
| status: ActiveStatus.ACTIVE, | ||
| }, | ||
| select: { id: true }, | ||
| }); | ||
|
|
||
| if (!userRecord) { | ||
| this.logger.debug( | ||
| `attachUser auth failed: user not found socket=${socketId} ip=${ip} sub=${subId.toString()}`, | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| const userId = Number(userRecord.id); | ||
|
|
||
| client.data.userId = userId; | ||
| client.join(toUserRoom(userId)); | ||
|
Check warning on line 144 in src/infra/websocket/auth/ws-auth.service.ts
|
||
|
|
||
| return userId; | ||
| } catch (e) { | ||
| const info = toErrorInfo(e); | ||
| this.logger.error( | ||
| `attachUser error: prisma query failed socket=${socketId} ip=${ip} sub=${subId.toString()} err=${info.name}:${info.message}`, | ||
| info.stack, | ||
| ); | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toPositiveBigIntfloors numericsubclaims (1.9becomes1) instead of rejecting them, andWsAuthService.attachUseruses that parsed value as the authenticated user ID. A signed token with a non-integer numericsubwould therefore authenticate as a different account, which is an identity-mapping bug in auth handling; this parser should require an integer value before converting.Useful? React with 👍 / 👎.