지난 시간에는 Redis에서 poll 데이터에 participants를 추가하고 제거하는 방법을 다루었으며, 클라이언트가 동일한 poll에 속한 다른 클라이언트와 통신하도록 했습니다.
오늘은 일부 이벤트에 대한 액세스를 관리자만 사용할 수 있도록 제한하는 방법을 살펴 볼것입니다.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
/**
* Guard that determines if the client is an admin
*/
@Injectable()
export class GatewayAdminGuard implements CanActivate {
/**
* Method that checks if the client is an admin
* @param context The execution context for the guard
* @returns A Promise containing a boolean indicating if the client is an admin
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
throw new Error('Method not implemented.');
}
}
canActivate 메소드 내에서 await 해야 할 로직이 있으므로 canActivate를 비동기로 명세합니다.
사실 다른 가드에서는 단순히 JSON Web Token 페이로드를 확인하고 추출했지만, 여기에서는 데이터베이스에 액세스하여 poll admin의 ID를 가져와 추출된 JWT의 ID와 비교 할 것입니다.
그러기 위해서는 먼저 PollService
와 JwtService
를 클래스에 inject해줘야 합니다.
private readonly logger = new Logger(GatewayAdminGuard.name);
constructor(
private readonly pollService: PollService,
private readonly jwtService: JwtService,
) {}
async getPoll(pollID: string): Promise<Poll> {
return this.pollsRepository.getPoll(pollID);
}
이를 통해 현재 소켓의 id를 얻을 수 있고 poll의 adminID
와 비교할 수 있습니다. 일치하지 않으면 WsUnauthorizedException
이 발생합니다.
또한 JWT를 파싱할 때 몇 가지 type 보정을 받으려고 합니다.
export type AuthPayload = {
userID: string;
pollID: string;
name: string;
};
canActivate
메서드의 마지막 부분을 살펴 볼게요.
// We define a type alias called SocketWithAuth to extend the regular Socket from socket.io.
const socket: SocketWithAuth = context.switchToWs().getClient();
// The token variable is set to the value of the token property of the handshake.auth object,
// or, if that property is undefined, to the value of the token property of the handshake.headers object.
const token =
socket.handshake.auth.token || socket.handshake.headers['token'];
// If the token variable is falsy (undefined, null, 0, false, NaN, or an empty string), then an error is thrown.
// The error message is logged and a WsUnauthorizedException is thrown.
if (!token) {
this.logger.error('No authorization token provided');
throw new WsUnauthorizedException('No token provided');
}
// If the token variable is truthy, we try to verify it using the verify method of the jwtService.
// The verify method returns a decoded payload if the token is valid, or throws an error if the token is invalid.
try {
const payload = this.jwtService.verify<AuthPayload & { sub: string }>(
token,
);
// If the token is valid, we log a debug message with the token payload.
this.logger.debug(Validating admin using token payload, payload);
// We destructure the sub and pollID properties from the token payload.
const { sub, pollID } = payload;
// We retrieve the poll from the pollsService using the getPoll method.
const poll = await this.pollsService.getPoll(pollID);
// If the sub property of the token payload is not equal to the adminID property of the poll, then an error is thrown.
// The error message is logged and a WsUnauthorizedException is thrown.
if (sub !== poll.adminID) {
throw new WsUnauthorizedException('Admin privileges required');
}
// If everything is successful, we return true.
return true;
} catch {
// If there is an error verifying the token, an error is thrown.
// The error message is logged and a WsUnauthorizedException is thrown.
throw new WsUnauthorizedException('Admin privileges required');
}
관리자만 접근 가능한 remove_participant
이벤트가 허용되는 PollsGateway에 추가하겠습니다.
/**
* This function removes a participant from a poll and emits an event to notify other clients about the update.
*
* @param id - The ID of the participant to remove.
* @param client - The client that initiated the request.
*/
@UseGuards(GatewayAdminGuard)
@SubscribeMessage('remove_participant')
async removeParticipant(
@MessageBody('id') id: string,
@ConnectedSocket() client: SocketWithAuth,
) {}
1) 데코레이터 2개를 추가합니다.
GatewayAdminGuard
가드를 사용하는 것이고 2) 함수 내부에서 파라미터를 정의합니다.
아래 코드 블록으로 함수 본문을 구현합니다.
this.logger.debug(
`Attempting to remove participant ${id} from poll ${client.pollID}`,
);
const updatedPoll = await this.pollsService.removeParticipant({
pollID: client.pollID,
userID: id,
});
//아래 code는 io 개체를 사용하여 pollID room을 수신하는 모든 클라이언트에게 poll_updated라는 이벤트를 보내게됩니다. updatedPoll 변수는 이벤트에 대한 데이터 페이로드로 전달되며 participant가 제거된 후 poll의 업데이트된 상태를 나타냅니다.
this.io.to(client.pollID).emit('poll_updated', updatedPoll);
}
handleConnect
과 handleDisconnection
메서드의 내부 로직중 room의 size가 undefined 될 수 있으므로 아래와 같이 코드를 수정하여 fallback을 0로 만들어 줍니다.
// correct in handleConnect and handleDisconnection
const clientCount = this.io.adapter.rooms?.get(roomName)?.size ?? 0;
ws-catch-all-filter
모듈에서 TypeException을 필터링 하기 위한 로직을 추가 하겠습니다.
if (exception instanceof WsTypeException) {
socket.emit('exception', exception.getError());
return;
}
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { SocketWithAuth } from 'src/polls/types';
import {
WsBadRequestException,
WsTypeException,
WsUnknownException,
} from './ws-exceptions';
/**
* Exception filter for WebSocket connections that handles all types of exceptions.
*/
@Catch()
export class WsCatchAllFilter implements ExceptionFilter {
/**
* Method that handles exceptions and sends them to the WebSocket client.
*
* @param exception The exception that was thrown.
* @param host The arguments host object containing the WebSocket client.
*/
catch(exception: Error, host: ArgumentsHost) {
const socket: SocketWithAuth = host.switchToWs().getClient(); // Get the WebSocket client from the host
if (exception instanceof BadRequestException) {
// If the exception is a BadRequestException
const exceptionData = exception.getResponse(); // Get the exception data
const exceptionMessage = // Extract the exception message from the data
exceptionData['message'] ?? exceptionData ?? exception.name;
const wsException = new WsBadRequestException(exceptionMessage); // Create a WebSocket-specific exception
socket.emit('exception', wsException.getError()); // Send the exception to the client
return; // End the method execution
}
if (exception instanceof WsTypeException) {
// If the exception is a WsTypeException
socket.emit('exception', exception.getError()); // Send the exception to the client
return; // End the method execution
}
const wsException = new WsUnknownException(exception.message); // Create a generic WebSocket exception
socket.emit('exception', wsException.getError()); // Send the exception to the client
}
}
1) WsCatchAllFilter는 모든 유형의 예외를 처리하는 WebSocket 연결에 대한 예외 필터입니다. catch 메서드를 구현해야 하는 @nestjs/common 패키지에서 제공하는 ExceptionFilter 인터페이스를 구현합니다.
2) catch 메서드는 예외와 호스트라는 두 가지 매개변수를 사용합니다. exception은 발생한 예외이고 host는 WebSocket 클라이언트를 포함하는 ArgumentsHost의 인스턴스입니다.
3) 이 메서드는 ArgumentsHost 클래스에서 제공하는 switchToWs 및 getClient 메서드를 사용하여 호스트에서 WebSocket 클라이언트를 가져오는 것으로 시작합니다.
4) 다음으로 메서드는 예외 유형을 확인합니다. BadRequestException 인스턴스인 경우 getResponse 메소드를 사용하여 응답 데이터에서 예외 메시지를 추출하여 WebSocket 특정 예외로 클라이언트에 보냅니다. WsTypeException의 인스턴스라면 예외를 그대로 클라이언트에 보냅니다. 예외가 다른 유형인 경우 예외 메시지와 함께 일반 WebSocket 예외를 클라이언트에 보냅니다.