Skip to content

Commit af1998a

Browse files
author
EpicFn
committed
feat : jwt 발급 로직 구현
1 parent 137141e commit af1998a

File tree

6 files changed

+143
-3
lines changed

6 files changed

+143
-3
lines changed

src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,6 @@ public ResponseEntity<RsData<BodyForReactFlow>> getGraph(
7171
"ID: " + dashboardId + " 의 React-flow 데이터를 조회했습니다.",
7272
BodyForReactFlow.from(graph)
7373
));
74-
7574
}
75+
7676
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.tuna.zoopzoop.backend.domain.dashboard.dto;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
public record ReqBodyForLiveblocksAuth(
7+
String userId,
8+
UserInfo userInfo,
9+
Map<String, List<String>>permissions
10+
) {
11+
public record UserInfo(
12+
String name,
13+
String avatar
14+
) {}
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.tuna.zoopzoop.backend.domain.dashboard.dto;
2+
3+
public record ResBodyForAuthToken(
4+
String token
5+
){ }

src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,24 @@
1010
import org.springframework.transaction.annotation.Transactional;
1111
import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow;
1212
import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage;
13+
import org.tuna.zoopzoop.backend.domain.dashboard.dto.ReqBodyForLiveblocksAuth;
1314
import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard;
1415
import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge;
1516
import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph;
1617
import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node;
1718
import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository;
1819
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
20+
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
21+
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
1922
import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService;
23+
import org.tuna.zoopzoop.backend.domain.space.space.entity.Space;
24+
import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService;
25+
import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient;
2026

2127
import java.nio.file.AccessDeniedException;
28+
import java.util.Collections;
2229
import java.util.List;
30+
import java.util.Map;
2331

2432
@Service
2533
@RequiredArgsConstructor
@@ -30,7 +38,8 @@ public class DashboardService {
3038
private final ObjectMapper objectMapper;
3139
private final SignatureService signatureService;
3240
private final RabbitTemplate rabbitTemplate;
33-
41+
private final SpaceService spaceService;
42+
private final LiveblocksClient liveblocksClient;
3443

3544

3645
// =========================== Graph 관련 메서드 ===========================
@@ -134,8 +143,59 @@ public void queueGraphUpdate(Integer dashboardId, String requestBody, String sig
134143
rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message);
135144
}
136145

146+
// =========================== 기타 메서드 ===========================
147+
148+
/**
149+
* 특정 스페이스에 대한 Liveblocks 접속 토큰(JWT)을 발급합니다.
150+
* @param spaceId 스페이스 ID
151+
* @param member 토큰을 요청하는 멤버
152+
* @return 발급된 JWT 문자열
153+
* @throws AccessDeniedException 멤버가 해당 스페이스에 속해있지 않거나 권한이 없는 경우
154+
*/
155+
@Transactional(readOnly = true)
156+
public String getAuthTokenForSpace(Integer spaceId, Member member) throws AccessDeniedException {
157+
Space space = spaceService.findById(spaceId);
158+
159+
// 해당 스페이스에 멤버가 속해있는지, PENDING 상태는 아닌지 확인
160+
Membership membership = membershipService.findByMemberAndSpace(member, space);
161+
if (membership.getAuthority().equals(Authority.PENDING)) {
162+
throw new AccessDeniedException("스페이스에 가입된 멤버가 아닙니다.");
163+
}
137164

165+
// Liveblocks Room ID 생성
166+
String roomId = "space_" + space.getId();
167+
168+
// Liveblocks에 전달할 사용자 정보 생성
169+
String userId = String.valueOf(member.getId());
170+
ReqBodyForLiveblocksAuth.UserInfo userInfo = new ReqBodyForLiveblocksAuth.UserInfo(
171+
member.getName(),
172+
member.getProfileImageUrl()
173+
);
174+
175+
// Liveblocks 권한 설정 (내 서비스의 Authority -> Liveblocks 권한으로 변환)
176+
List<String> permissions;
177+
switch (membership.getAuthority()) {
178+
case ADMIN, READ_WRITE:
179+
permissions = List.of("room:write");
180+
break;
181+
case READ_ONLY:
182+
permissions = Collections.emptyList(); // 빈 리스트는 읽기 전용을 의미
183+
break;
184+
default:
185+
// PENDING 등 다른 상태는 위에서 이미 필터링됨
186+
throw new AccessDeniedException("유효하지 않은 권한입니다.");
187+
}
138188

189+
// Liveblocks Client에 전달할 요청 객체 생성
190+
ReqBodyForLiveblocksAuth authRequest = new ReqBodyForLiveblocksAuth(
191+
userId,
192+
userInfo,
193+
Map.of(roomId, permissions)
194+
);
195+
196+
// LiveblocksClient를 통해 토큰 발급 요청
197+
return liveblocksClient.getAuthToken(authRequest);
198+
}
139199

140200

141201
}

src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
import org.springframework.data.domain.Pageable;
99
import org.springframework.data.domain.Sort;
1010
import org.springframework.data.web.PageableDefault;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
1113
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1214
import org.springframework.web.bind.annotation.*;
1315
import org.springframework.web.multipart.MultipartFile;
16+
import org.tuna.zoopzoop.backend.domain.dashboard.dto.ResBodyForAuthToken;
17+
import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService;
1418
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1519
import org.tuna.zoopzoop.backend.domain.space.membership.dto.etc.SpaceMemberInfo;
1620
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
@@ -38,6 +42,7 @@
3842
public class ApiV1SpaceController {
3943
private final SpaceService spaceService;
4044
private final MembershipService membershipService;
45+
private final DashboardService dashboardService;
4146

4247
@PostMapping
4348
@Operation(summary = "스페이스 생성")
@@ -206,5 +211,30 @@ public RsData<ResBodyForSpaceInfo> getSpace(
206211
);
207212
}
208213

214+
/**
215+
* Liveblocks 접속을 위한 인증 토큰(JWT) 발급 API
216+
* @param spaceId 스페이스 ID
217+
* @param userDetails 현재 로그인한 사용자 정보
218+
* @return ResponseEntity<RsData<AuthTokenResponse>>
219+
*/
220+
@PostMapping("/dashboard-auth/{spaceId}")
221+
@Operation(summary = "Liveblocks 접속 토큰 발급")
222+
public ResponseEntity<RsData<ResBodyForAuthToken>> getAuthToken(
223+
@PathVariable Integer spaceId,
224+
@AuthenticationPrincipal CustomUserDetails userDetails) throws AccessDeniedException {
225+
226+
Member member = userDetails.getMember();
227+
String token = dashboardService.getAuthTokenForSpace(spaceId, member);
228+
229+
ResBodyForAuthToken response = new ResBodyForAuthToken(token);
230+
231+
return ResponseEntity
232+
.status(HttpStatus.OK)
233+
.body(new RsData<>(
234+
"200",
235+
"Liveblocks 접속 토큰이 발급되었습니다.",
236+
response
237+
));
238+
}
209239

210240
}

src/main/java/org/tuna/zoopzoop/backend/global/clients/liveblocks/LiveblocksClient.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import org.springframework.stereotype.Component;
88
import org.springframework.web.client.RestClientException;
99
import org.springframework.web.client.RestTemplate;
10+
import org.tuna.zoopzoop.backend.domain.dashboard.dto.ReqBodyForLiveblocksAuth;
11+
import org.tuna.zoopzoop.backend.domain.dashboard.dto.ResBodyForAuthToken;
1012

1113
import java.util.Collections;
1214
import java.util.HashMap;
@@ -23,7 +25,7 @@ public class LiveblocksClient {
2325
private String secretKey;
2426

2527
private static final String LIVEBLOCKS_API_URL = "https://api.liveblocks.io/v2/rooms";
26-
28+
private static final String AUTH_API_URL = "https://api.liveblocks.io/v2/authorize-user";
2729
/**
2830
* Liveblocks 서버에 새로운 방을 생성합니다.
2931
* @param roomId 생성할 방의 고유 ID (워크스페이스 ID와 동일하게 사용)
@@ -80,4 +82,32 @@ public void deleteRoom(String roomId) {
8082
throw new RuntimeException("Liveblocks API call failed", e);
8183
}
8284
}
85+
86+
/**
87+
* Liveblocks 사용자 인증 토큰(JWT)을 발급받습니다.
88+
* @param request 인증에 필요한 사용자 정보, 권한 등을 담은 객체
89+
* @return 발급된 JWT 문자열
90+
*/
91+
public String getAuthToken(ReqBodyForLiveblocksAuth request) {
92+
HttpHeaders headers = new HttpHeaders();
93+
headers.setBearerAuth(secretKey);
94+
headers.setContentType(MediaType.APPLICATION_JSON);
95+
96+
HttpEntity<ReqBodyForLiveblocksAuth> requestEntity = new HttpEntity<>(request, headers);
97+
98+
try {
99+
ResponseEntity<String> response = restTemplate.postForEntity(AUTH_API_URL, requestEntity, String.class);
100+
if (response.getStatusCode().is2xxSuccessful()) {
101+
log.info("Liveblocks auth token issued successfully for user: {}", request.userId());
102+
return response.getBody();
103+
} else {
104+
log.error("Failed to issue Liveblocks auth token. user: {}, status: {}, body: {}",
105+
request.userId(), response.getStatusCode(), response.getBody());
106+
throw new RuntimeException("Failed to issue Liveblocks auth token.");
107+
}
108+
} catch (RestClientException e) {
109+
log.error("Error while calling Liveblocks auth API for user: {}", request.userId(), e);
110+
throw new RuntimeException("Liveblocks auth API call failed", e);
111+
}
112+
}
83113
}

0 commit comments

Comments
 (0)