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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) {
throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER);
}

String email = authManager.resolveAccessToken(token);
String email = authManager.resolveChairmanToken(token);
return authService.getMember(email);
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.debatetimer.config.sharing;

import com.debatetimer.config.CorsProperties;
import java.time.Duration;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
Expand All @@ -15,6 +18,12 @@
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private static final long SERVER_TO_CLIENT_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long CLIENT_TO_SERVER_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long SOCKJS_HEART_BEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final String HEART_BEAT_THREAD_PREFIX = "wss-heartbeat-";
private static final int HEART_BEAT_THREAD_COUNT = 1;

private final CorsProperties corsProperties;
private final WebSocketAuthMemberResolver webSocketAuthMemberResolver;

Expand All @@ -25,14 +34,26 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/room", "/chairman");
registry.enableSimpleBroker("/room", "/chairman")
.setHeartbeatValue(new long[]{SERVER_TO_CLIENT_HEARTBEAT_DURATION, CLIENT_TO_SERVER_HEARTBEAT_DURATION})
.setTaskScheduler(heartBeatScheduler());
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns(corsProperties.getCorsOrigin())
.withSockJS();
.withSockJS()
.setHeartbeatTime(SOCKJS_HEART_BEAT_DURATION);
}

@Bean
public ThreadPoolTaskScheduler heartBeatScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(HEART_BEAT_THREAD_COUNT);
scheduler.setThreadNamePrefix(HEART_BEAT_THREAD_PREFIX);
scheduler.initialize();
return scheduler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.debatetimer.controller.sharing;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.controller.tool.jwt.AuthManager;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.response.ChairmanTokenResponse;
import com.debatetimer.service.customize.CustomizeService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class SharingRestController {

private final CustomizeService customizeService;
private final AuthManager authManager;

@GetMapping("/api/share/{tableId}/chairman-token")
public ChairmanTokenResponse issueChairmanToken(
@AuthMember Member member,
@PathVariable("tableId") long tableId
) {
long debateTime = customizeService.findDebateTime(tableId, member);
String chairmanToken = authManager.issueChairmanToken(member, debateTime * 2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[1] AuthManager.issueChairmanToken 시그니처/만료 단위 확인"
fd -i 'AuthManager.java' src | xargs -r sed -n '1,260p'
rg -n --type=java -C2 'issueChairmanToken|expire|expiration|ttl' src

echo "[2] 토론 시간 입력값 상한 검증 확인"
fd -i 'CustomizeTimeBoxCreateRequest.java' src | xargs -r sed -n '1,260p'
rg -n --type=java -C2 '@Max|@Min|@Positive|@PositiveOrZero|time|duration' src/main/java/com/debatetimer

Repository: debate-timer/debate-timer-be

Length of output: 50385


debateTime * 2 곱셈 오버플로우 방어 추가 권장

line 26의 long 타입 곱셈은 오버플로우 시 조용히 음수 또는 비정상 값을 발생시킵니다. debateTime이 매우 큰 값이면 곱셈 결과가 음수가 되어 issueChairmanToken에 부정확한 TTL이 전달될 수 있으므로 Math.multiplyExact()를 사용한 방어가 좋습니다.

제안 코드
-        String chairmanToken = authManager.issueChairmanToken(member, debateTime * 2);
+        long chairmanTtl = Math.multiplyExact(debateTime, 2L);
+        String chairmanToken = authManager.issueChairmanToken(member, chairmanTtl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String chairmanToken = authManager.issueChairmanToken(member, debateTime * 2);
long chairmanTtl = Math.multiplyExact(debateTime, 2L);
String chairmanToken = authManager.issueChairmanToken(member, chairmanTtl);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/debatetimer/controller/sharing/SharingRestController.java`
at line 26, The multiplication debateTime * 2 in SharingRestController when
calling authManager.issueChairmanToken can overflow for large debateTime; change
the call to compute the TTL using Math.multiplyExact(debateTime, 2) (or catch
ArithmeticException) before passing to issueChairmanToken so you either
propagate/handle the error or clamp/validate the TTL—ensure you reference
debateTime, Math.multiplyExact, and authManager.issueChairmanToken when locating
and updating the code to prevent silent overflow.

return new ChairmanTokenResponse(chairmanToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.debatetimer.controller.sharing;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.request.SharingRequest;
import com.debatetimer.dto.sharing.response.SharingResponse;
import com.debatetimer.service.sharing.SharingService;
Expand All @@ -13,17 +15,17 @@

@Controller
@RequiredArgsConstructor
public class SharingController {
public class SharingWebSocketController {

private final SharingService sharingService;

@MessageMapping("/event/{roomId}")
@SendTo("/room/{roomId}")
public SharingResponse share(
@AuthMember Member member,
@DestinationVariable(value = "roomId") long roomId,
@Valid @Payload SharingRequest request
) {

return sharingService.share(request);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.debatetimer.controller.tool.jwt;

import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.member.JwtTokenResponse;
import com.debatetimer.dto.member.MemberInfo;
import java.time.Duration;
Expand All @@ -20,6 +21,11 @@ public JwtTokenResponse issueToken(MemberInfo memberInfo) {
return new JwtTokenResponse(accessToken, refreshToken, refreshTokenExpiration);
}

public String issueChairmanToken(Member member, long expiration) {
MemberInfo memberInfo = new MemberInfo(member.getEmail());
return jwtTokenProvider.createChairmanToken(memberInfo, expiration);
}

public JwtTokenResponse reissueToken(String refreshToken) {
String email = jwtTokenResolver.resolveRefreshToken(refreshToken);
MemberInfo memberInfo = new MemberInfo(email);
Expand All @@ -37,4 +43,8 @@ public String resolveAccessToken(String accessToken) {
public String resolveRefreshToken(String refreshToken) {
return jwtTokenResolver.resolveRefreshToken(refreshToken);
}

public String resolveChairmanToken(String chairmanToken) {
return jwtTokenResolver.resolveChairmanToken(chairmanToken);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String createRefreshToken(MemberInfo memberInfo) {
return createToken(memberInfo, refreshTokenExpiration, TokenType.REFRESH_TOKEN);
}

public String createChairmanToken(MemberInfo memberInfo, long expirationSeconds) {
return createToken(memberInfo, Duration.ofSeconds(expirationSeconds), TokenType.CHAIRMAN_TOKEN);
}

private String createToken(MemberInfo memberInfo, Duration expiration, TokenType tokenType) {
Date now = new Date();
Date expiredDate = new Date(now.getTime() + expiration.toMillis());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String resolveRefreshToken(String refreshToken) {
return resolveToken(refreshToken, TokenType.REFRESH_TOKEN);
}

public String resolveChairmanToken(String chairmanToken) {
return resolveToken(chairmanToken, TokenType.CHAIRMAN_TOKEN);
}

private String resolveToken(String token, TokenType tokenType) {
try {
Claims claims = Jwts.parserBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum TokenType {

ACCESS_TOKEN,
REFRESH_TOKEN
REFRESH_TOKEN,
CHAIRMAN_TOKEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public List<CustomizeTimeBox> getCustomizeTimeBoxes(long tableId, Member member)
return toCustomizeTimeBoxes(timeBoxEntityList, bellEntityList);
}

@Transactional(readOnly = true)
public long getTotalTimeBoxTimes(long tableId) {
return timeBoxRepository.sumTimeByTableId(tableId);
}

private List<CustomizeTimeBox> toCustomizeTimeBoxes(
List<CustomizeTimeBoxEntity> timeBoxEntities,
List<BellEntity> bellEntities
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.debatetimer.dto.sharing.response;

public record ChairmanTokenResponse(
String chairmanToken
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public interface CustomizeTimeBoxRepository extends Repository<CustomizeTimeBoxE

List<CustomizeTimeBoxEntity> findAllByCustomizeTable(CustomizeTableEntity table);

@Query("SELECT COALESCE(SUM(ctb.time), 0) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
long sumTimeByTableId(long tableId);

@Query("DELETE FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
@Modifying(clearAutomatically = true, flushAutomatically = true)
void deleteAllByTable(long tableId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public CustomizeTableResponse findTable(long tableId, Member member) {
return new CustomizeTableResponse(table, timeBoxes);
}

@Transactional(readOnly = true)
public long findDebateTime(long tableId, Member member) {
CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member);
return customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId());
}

@Transactional
public CustomizeTableResponse updateTable(
CustomizeTableCreateRequest tableCreateRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import com.debatetimer.dto.sharing.response.SharingResponse;
import com.debatetimer.dto.sharing.response.TimerEventDataResponse;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SharingService {

public SharingResponse share(SharingRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.debatetimer.service.organization.OrganizationService;
import com.debatetimer.service.poll.PollService;
import com.debatetimer.service.poll.VoteService;
import com.debatetimer.service.sharing.SharingService;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.filter.log.RequestLoggingFilter;
Expand Down Expand Up @@ -71,6 +72,9 @@ public abstract class BaseDocumentTest {
@MockitoBean
protected VoteService voteService;

@MockitoBean
protected SharingService sharingService;

@MockitoBean
protected OrganizationService organizationService;

Expand Down
1 change: 1 addition & 0 deletions src/test/java/com/debatetimer/controller/Tag.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum Tag {
PARLIAMENTARY_API("Parliamentary Table API"),
TIME_BASED_API("Time Based Table API"),
CUSTOMIZE_API("Customize Table API"),
SHARING_API("Sharing API"),
POLL_API("Poll API"),
ORGANIZATION_API("Organization API");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.debatetimer.controller.sharing;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;

import com.debatetimer.controller.BaseDocumentTest;
import com.debatetimer.controller.RestDocumentationRequest;
import com.debatetimer.controller.RestDocumentationResponse;
import com.debatetimer.controller.Tag;
import com.debatetimer.domain.member.Member;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;

public class SharingDocumentTest extends BaseDocumentTest {

@Nested
Comment thread
coli-geonwoo marked this conversation as resolved.
class IssueChairmanToken {

private final RestDocumentationRequest requestDocument = request()
.tag(Tag.SHARING_API)
.summary("사회자용 토큰 발급")
.requestHeader(headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰"))
.pathParameter(parameterWithName("tableId").description("테이블 id"));

private final RestDocumentationResponse responseDocument = response()
.responseBodyField(
fieldWithPath("chairmanToken").type(STRING).description("사회자용 토큰")
);

@Test
void 사회자_용_토큰_생성_성공() {
long requestTableId = 1L;
long debateTime = 500L;
doReturn(debateTime).when(customizeService)
.findDebateTime(eq(requestTableId), any(Member.class));
doReturn("testToken").when(authManager)
.issueChairmanToken(any(Member.class), eq(debateTime * 2));

var document = document("sharing/get", 200)
.request(requestDocument)
.response(responseDocument)
.build();

given(document)
.contentType(ContentType.JSON)
.headers(EXIST_MEMBER_HEADER)
.pathParam("tableId", String.valueOf(requestTableId))
.when().get("/api/share/{tableId}/chairman-token")
.then().statusCode(200);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import org.junit.jupiter.params.provider.NullSource;
import org.springframework.messaging.simp.stomp.StompHeaders;

class SharingControllerTest extends BaseStompTest {
class SharingWebSocketControllerTest extends BaseStompTest {

@Nested
class Share {
Expand All @@ -31,7 +31,7 @@ class Share {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
Expand All @@ -58,12 +58,34 @@ class Share {
);
}

@Test
void 사회자가_아니면_이벤트를_발행할_수_없다() throws ExecutionException, InterruptedException, TimeoutException {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
CustomizeBoxType.NORMAL,
null,
2,
30
)
);
stompSession.subscribe("/room/" + roomId, handler); //청중의 구독
stompSession.send("/app/event/" + roomId, request); //사회자의 이벤트 발생

assertThatThrownBy(() -> handler.getCompletableFuture()
.get(2L, TimeUnit.SECONDS))
.isInstanceOf(TimeoutException.class);
}
Comment thread
coli-geonwoo marked this conversation as resolved.

@Test
void 사회자가_발생시킨_토론_종료_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(TimerEventType.FINISHED, null);
stompSession.subscribe("/room/" + roomId, handler); //청중의 구독

Expand All @@ -84,7 +106,7 @@ class Share {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
Expand Down
Loading
Loading