Skip to content

Commit c7ff25a

Browse files
authored
feat(be): add session termination (#214)
feat: add session termination
1 parent 7cb91da commit c7ff25a

File tree

8 files changed

+95
-6
lines changed

8 files changed

+95
-6
lines changed

apps/server/src/common/guards/session-token-validation.guard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class SessionTokenValidationGuard implements CanActivate {
1212

1313
async canActivate(context: ExecutionContext) {
1414
const request = context.switchToHttp().getRequest();
15-
const sessionId = request.body?.sessionId || request.query?.sessionId;
15+
const sessionId = request.body?.sessionId || request.query?.sessionId || request.params?.sessionId;
1616
const token = request.body?.token || request.query?.token;
1717

1818
if (!sessionId || !token) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class TerminateSessionDto {
5+
@ApiProperty({
6+
example: 'user_token_123',
7+
description: '사용자의 토큰',
8+
required: true,
9+
})
10+
@IsString()
11+
@IsNotEmpty({ message: '사용자 토큰은 필수입니다.' })
12+
token: string;
13+
}
Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
1-
import { Body, Controller, Get, Post, Req, UseGuards, UseInterceptors } from '@nestjs/common';
1+
import { Body, Controller, Get, HttpCode, Param, Post, Req, UseGuards, UseInterceptors } from '@nestjs/common';
22
import { ApiBody, ApiTags } from '@nestjs/swagger';
33

44
import { CreateSessionDto } from './dto/create-session.dto';
5+
import { TerminateSessionDto } from './dto/terminate-session.dto';
56
import { SessionsService } from './sessions.service';
67
import { CreateSessionSwagger } from './swagger/create-session.swagger';
78
import { GetSessionSwagger } from './swagger/get-session.swagger';
9+
import { TerminateSessionSwagger } from './swagger/terminate-session.swagger';
810

911
import { JwtAuthGuard } from '@auth/jwt-auth.guard';
12+
import { SessionTokenValidationGuard } from '@common/guards/session-token-validation.guard';
1013
import { TransformInterceptor } from '@common/interceptors/transform.interceptor';
14+
import { SocketGateway } from '@socket/socket.gateway';
1115

1216
@ApiTags('Sessions')
1317
@UseInterceptors(TransformInterceptor)
14-
@UseGuards(JwtAuthGuard)
1518
@Controller('sessions')
1619
export class SessionsController {
17-
constructor(private readonly sessionsService: SessionsService) {}
20+
constructor(
21+
private readonly sessionsService: SessionsService,
22+
private readonly socketGateway: SocketGateway,
23+
) {}
1824

1925
@Post()
26+
@UseGuards(JwtAuthGuard)
2027
@CreateSessionSwagger()
2128
@ApiBody({ type: CreateSessionDto })
2229
async create(@Body() createSessionDto: CreateSessionDto, @Req() request: Request) {
@@ -26,10 +33,21 @@ export class SessionsController {
2633
}
2734

2835
@Get()
36+
@UseGuards(JwtAuthGuard)
2937
@GetSessionSwagger()
3038
async getSessionsById(@Req() request: Request) {
3139
const userId = request['user'].userId;
3240
const sessionData = await this.sessionsService.getSessionsById(userId);
3341
return { sessionData };
3442
}
43+
44+
@Post(':sessionId/terminate')
45+
@HttpCode(200)
46+
@TerminateSessionSwagger()
47+
@UseGuards(SessionTokenValidationGuard)
48+
async terminateSession(@Param('sessionId') sessionId: string, @Body() { token }: TerminateSessionDto) {
49+
const result = await this.sessionsService.terminateSession(sessionId, token);
50+
this.socketGateway.broadcastSessionEnd(sessionId, token, result);
51+
return result;
52+
}
3553
}

apps/server/src/sessions/sessions.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import { SessionsRepository } from './sessions.repository';
66
import { SessionsService } from './sessions.service';
77

88
import { AuthModule } from '@auth/auth.module';
9+
import { SessionTokenModule } from '@common/guards/session-token.module';
910
import { PrismaModule } from '@prisma-alias/prisma.module';
1011
import { SessionsAuthRepository } from '@sessions-auth/sessions-auth.repository';
12+
import { SocketModule } from '@socket/socket.module';
13+
1114
@Module({
12-
imports: [PrismaModule, JwtModule.register({}), AuthModule],
15+
imports: [PrismaModule, JwtModule.register({}), AuthModule, SessionTokenModule, SocketModule],
1316
controllers: [SessionsController],
1417
providers: [SessionsService, SessionsRepository, SessionsAuthRepository],
1518
exports: [SessionsService],

apps/server/src/sessions/sessions.repository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,17 @@ export class SessionsRepository {
5151
throw DatabaseException.read('UserSessionToken');
5252
}
5353
}
54+
55+
async updateSessionExpiredAt(sessionId: string, expireTime: Date) {
56+
try {
57+
await this.prisma.session.update({
58+
where: {
59+
sessionId,
60+
},
61+
data: { expiredAt: expireTime },
62+
});
63+
} catch (error) {
64+
throw DatabaseException.update('session');
65+
}
66+
}
5467
}

apps/server/src/sessions/sessions.service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { ForbiddenException, Injectable } from '@nestjs/common';
22

33
import { CreateSessionDto } from './dto/create-session.dto';
44
import { SessionCreateData } from './interface/session-create-data.interface';
@@ -52,4 +52,18 @@ export class SessionsService {
5252
});
5353
return transformedSessions;
5454
}
55+
56+
async terminateSession(sessionId: string, token: string) {
57+
const [{ createUserId }, { userId }] = await Promise.all([
58+
this.sessionRepository.findById(sessionId),
59+
this.sessionsAuthRepository.findByToken(token),
60+
]);
61+
62+
if (createUserId !== userId) {
63+
throw new ForbiddenException('세션 생성자만이 이 작업을 수행할 수 있습니다.');
64+
}
65+
const expireTime = new Date();
66+
await this.sessionRepository.updateSessionExpiredAt(sessionId, expireTime);
67+
return { expired: true };
68+
}
5569
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
3+
4+
export const TerminateSessionSwagger = () =>
5+
applyDecorators(
6+
ApiOperation({ summary: '세션 만료' }),
7+
ApiResponse({
8+
status: 200,
9+
description: '세션 만료작업 성공',
10+
schema: {
11+
example: {
12+
expired: true,
13+
},
14+
},
15+
}),
16+
ApiResponse({
17+
status: 403,
18+
description: '세션 만료 작업 실패',
19+
schema: {
20+
example: {
21+
message: '세션 생성자만이 이 작업을 수행할 수 있습니다.',
22+
},
23+
},
24+
}),
25+
);

apps/server/src/socket/socket.gateway.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const SOCKET_EVENTS = {
3232
PARTICIPANT_COUNT_UPDATED: 'participantCountUpdated',
3333

3434
HOST_CHANGED: 'hostChanged',
35+
SESSION_ENDED: 'sessionEnded',
3536
} as const;
3637

3738
interface Client {
@@ -162,4 +163,6 @@ export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
162163
broadcastReplyLike = this.createEventBroadcaster(SOCKET_EVENTS.REPLY_LIKED);
163164

164165
broadcastHostChange = this.createEventBroadcaster(SOCKET_EVENTS.HOST_CHANGED);
166+
167+
broadcastSessionEnd = this.createEventBroadcaster(SOCKET_EVENTS.SESSION_ENDED);
165168
}

0 commit comments

Comments
 (0)