Skip to content

Commit

Permalink
Eventクラスの作成、各種Guardの作成などによるRoomモジュールのリファクタ (#143)
Browse files Browse the repository at this point in the history
* [backend] Add kick guard and refactor room controller
* [backend] Add ChangeRoleGuard and refactor updateUserOnRoom
* [backend] Add OwnerGuard and refactor room.controller.ts
* [backend] Refactor room controller to have controller level guards
  • Loading branch information
usatie authored Dec 16, 2023
1 parent 7019a2e commit bae017a
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 141 deletions.
4 changes: 4 additions & 0 deletions backend/src/common/events/room-left.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class RoomLeftEvent {
roomId: number;
userId: number;
}
File renamed without changes.
70 changes: 70 additions & 0 deletions backend/src/room/guards/change-role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { UpdateUserOnRoomDto } from '../dto/update-UserOnRoom.dto';

@Injectable()
export class ChangeRoleGuard implements CanActivate {
constructor(private prisma: PrismaService) {}

expectNumberParam(
param: string | null | undefined,
paramName: string,
): number {
if (!param) {
throw new BadRequestException(`${paramName} is required`);
}
if (!/^\d+$/.test(param)) {
throw new BadRequestException(`${paramName} must be a number`);
}
return Number(param);
}

async canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const { params, member } = req;
if (!member) {
throw new ForbiddenException('require member');
}
// Validate roomId and targetUserId(userId)
const roomId = this.expectNumberParam(params.roomId, 'roomId');
const targetUserId = this.expectNumberParam(params.userId, 'userId');

// Check if targetUser is a member of the room
const userOnRoom = await this.prisma.userOnRoom.findUniqueOrThrow({
where: {
userId_roomId_unique: {
userId: targetUserId,
roomId: roomId,
},
},
});
const targetRole = userOnRoom.role;

// If member is trying to change someone else's role throw a ForbiddenException
if (member.role === Role.MEMBER) {
throw new ForbiddenException('Members cannot change the role of others');
}

// If admin is trying to kick owner, throw a ForbiddenException
if (targetRole === Role.OWNER && member.role === Role.ADMINISTRATOR) {
throw new ForbiddenException('Admins cannot change the role of owner');
}

// Cannot change the role to be owner
// Ownership should be transferred by owner
const dto: UpdateUserOnRoomDto = req.body;
if (dto.role === Role.OWNER) {
throw new ForbiddenException('Anyone cannot change the role to be owner');
}

// Otherwise, admin/owner kicking someone else, that's ok
return true;
}
}
69 changes: 69 additions & 0 deletions backend/src/room/guards/kick.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class KickGuard implements CanActivate {
constructor(private prisma: PrismaService) {}

expectNumberParam(
param: string | null | undefined,
paramName: string,
): number {
if (!param) {
throw new BadRequestException(`${paramName} is required`);
}
if (!/^\d+$/.test(param)) {
throw new BadRequestException(`${paramName} must be a number`);
}
return Number(param);
}

async canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const { params, user, member } = req;
if (!user || !member) {
throw new ForbiddenException('require login and member');
}
// Validate roomId and targetUserId(userId)
const roomId = this.expectNumberParam(params.roomId, 'roomId');
const targetUserId = this.expectNumberParam(params.userId, 'userId');

// Check if targetUser is a member of the room
// If target user is not found, it's okay to return NotFoundException,
// So I don't want to implement any try/catch here.
const userOnRoom = await this.prisma.userOnRoom.findUniqueOrThrow({
where: {
userId_roomId_unique: {
userId: targetUserId,
roomId: roomId,
},
},
});
const targetRole = userOnRoom.role;

// If anyone is trying to kick themself, that's ok
if (targetUserId === user.id) {
return true;
}

// If member is trying to kick someone else throw a ForbiddenException
if (member.role === Role.MEMBER) {
throw new ForbiddenException('Members cannot kick others');
}

// If admin is trying to kick owner, throw a ForbiddenException
if (targetRole === Role.OWNER && member.role === Role.ADMINISTRATOR) {
throw new ForbiddenException('Admins cannot kick owners');
}

// Otherwise, admin/owner kicking someone else, that's ok
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { RoomService } from './room.service';
import { RoomService } from '../room.service';

@Injectable()
export class MemberGuard implements CanActivate {
Expand All @@ -14,6 +14,9 @@ export class MemberGuard implements CanActivate {
async canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const { params, user } = req;
if (!user) {
throw new ForbiddenException('require login');
}
const { roomId } = params;
if (!roomId) {
throw new Error('MemberGuard should only be used with :roomId');
Expand Down
63 changes: 63 additions & 0 deletions backend/src/room/guards/owner.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class OwnerGuard implements CanActivate {
constructor(private prisma: PrismaService) {}

expectNumberParam(
param: string | null | undefined,
paramName: string,
): number {
if (!param) {
throw new BadRequestException(`${paramName} is required`);
}
if (!/^\d+$/.test(param)) {
throw new BadRequestException(`${paramName} must be a number`);
}
return Number(param);
}

async canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
const { params, user } = req;
// Validate roomId and targetUserId(userId)
const roomId = this.expectNumberParam(params.roomId, 'roomId');
if (!user) {
throw new ForbiddenException('require login');
}

// Check if targetUser is a member of the room
let userOnRoom;
try {
userOnRoom = await this.prisma.userOnRoom.findUniqueOrThrow({
where: {
userId_roomId_unique: {
userId: user.id,
roomId: roomId,
},
},
});
} catch (e) {
if (e.code === 'P2025') {
throw new ForbiddenException('User not found in the room');
} else {
throw e;
}
}

// Check if user is the owner
if (userOnRoom.role !== Role.OWNER) {
throw new ForbiddenException('Only owner can do this');
}
req.member = userOnRoom;
return true;
}
}
41 changes: 16 additions & 25 deletions backend/src/room/room.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ import { RoomEntity } from './entities/room.entity';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { UserOnRoomEntity } from './entities/UserOnRoom.entity';
import { UpdateUserOnRoomDto } from './dto/update-UserOnRoom.dto';
import { MemberGuard } from './member.guard';
import { MemberGuard } from './guards/member.guard';
import { CurrentUser } from 'src/common/decorators/current-user.decorator';
import { User } from '@prisma/client';
import { Member } from './member.decorator';
import { Member } from './decorators/member.decorator';
import { KickGuard } from './guards/kick.guard';
import { ChangeRoleGuard } from './guards/change-role.guard';
import { OwnerGuard } from './guards/owner.guard';

@Controller('room')
@UseGuards(JwtAuthGuard)
@ApiTags('room')
export class RoomController {
constructor(private readonly roomService: RoomService) {}
@Post()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiCreatedResponse({ type: RoomEntity })
create(@Body() createRoomDto: CreateRoomDto, @CurrentUser() user: User) {
Expand All @@ -48,42 +51,37 @@ export class RoomController {
}

@Get(':roomId')
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(MemberGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: RoomEntity })
findOne(@Member() member: UserOnRoomEntity) {
return this.roomService.findRoom(member.roomId);
}

@Patch(':roomId')
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(OwnerGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: RoomEntity })
update(
@Body() updateRoomDto: UpdateRoomDto,
@Member() member: UserOnRoomEntity,
) {
return this.roomService.updateRoom(
member.roomId,
updateRoomDto,
member.role,
);
return this.roomService.updateRoom(member.roomId, updateRoomDto);
}

@Delete(':roomId')
@HttpCode(204)
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(OwnerGuard)
@ApiBearerAuth()
@ApiNoContentResponse()
removeRoom(
@Param('roomId', ParseIntPipe) roomId: number,
@Member() member: UserOnRoomEntity,
) {
return this.roomService.removeRoom(member.roomId, member.role);
return this.roomService.removeRoom(member.roomId);
}

@Post(':roomId')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: RoomEntity })
createUserOnRoom(
Expand All @@ -94,7 +92,7 @@ export class RoomController {
}

@Get(':roomId/:userId')
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(MemberGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserOnRoomEntity })
getUserOnRoom(
Expand All @@ -106,24 +104,18 @@ export class RoomController {

@Delete(':roomId/:userId')
@HttpCode(204)
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(MemberGuard, KickGuard)
@ApiBearerAuth()
@ApiNoContentResponse()
async deleteUserOnRoom(
deleteUserOnRoom(
@Param('userId', ParseIntPipe) userId: number,
@CurrentUser() user: User,
@Member() member: UserOnRoomEntity,
) {
await this.roomService.removeUserOnRoom(
member.roomId,
member.role,
userId,
user,
);
return this.roomService.kickUser(member.roomId, userId);
}

@Patch(':roomId/:userId')
@UseGuards(JwtAuthGuard, MemberGuard)
@UseGuards(MemberGuard, ChangeRoleGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserOnRoomEntity })
updateUserOnRoom(
Expand All @@ -133,7 +125,6 @@ export class RoomController {
) {
return this.roomService.updateUserOnRoom(
member.roomId,
member.role,
userId,
updateUserOnRoomDto,
);
Expand Down
Loading

0 comments on commit bae017a

Please sign in to comment.