Skip to content

Commit e7e45d1

Browse files
committed
Merge remote-tracking branch 'origin/develop' into OPS-252-BE-feat-조건별-자료-검색
2 parents 0994208 + 21834c4 commit e7e45d1

File tree

59 files changed

+1447
-314
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1447
-314
lines changed

.github/workflows/test-server-ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
# ==================================
2121
ci:
2222
runs-on: ubuntu-latest
23+
env:
24+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
2325

2426
steps:
2527
# 1. 소스 코드 체크아웃
@@ -61,6 +63,8 @@ jobs:
6163
echo "spring.cloud.aws.s3.bucket: ${{ secrets.AWS_S3_BUCKET_NAME }}" >> src/main/resources/application-secrets.yml
6264
echo "spring.cloud.aws.stack.auto: false" >> src/main/resources/application-secrets.yml
6365
66+
echo "liveblocks.secret-key: ${{ secrets.LIVEBLOCKS_SECRET_KEY }}" >> src/main/resources/application-secrets.yml
67+
6468
# 6. application-secrets-server.yml 생성
6569
- name: Generate application-secrets-server.yml
6670
run: |

build.gradle

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
id 'java'
33
id 'org.springframework.boot' version '3.5.5'
44
id 'io.spring.dependency-management' version '1.1.7'
5+
id "io.sentry.jvm.gradle" version "5.12.0"
56
}
67

78
group = 'org.tuna.zoopzoop'
@@ -105,7 +106,12 @@ dependencies {
105106

106107
// Playwright for Java
107108
implementation 'com.microsoft.playwright:playwright:1.54.0'
108-
109+
110+
// Sentry (모니터링 용)
111+
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:8.22.0'
112+
113+
// Apache Commons Codec
114+
implementation"commons-codec:commons-codec:1.19.0"
109115
}
110116

111117
dependencyManagement {
@@ -117,3 +123,14 @@ dependencyManagement {
117123
tasks.named('test') {
118124
useJUnitPlatform()
119125
}
126+
127+
sentry {
128+
// Generates a JVM (Java, Kotlin, etc.) source bundle and uploads your source code to Sentry.
129+
// This enables source context, allowing you to see your source
130+
// code as part of your stack traces in Sentry.
131+
includeSourceContext = true
132+
133+
org = "whitedoggy"
134+
projectName = "zoopzoop-backend"
135+
authToken = System.getenv("SENTRY_AUTH_TOKEN")
136+
}

src/main/java/org/tuna/zoopzoop/backend/domain/archive/folder/repository/FolderRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,8 @@ public interface FolderRepository extends JpaRepository<Folder, Integer>{
5858
""")
5959
Optional<Folder> findByIdAndMemberId(@Param("folderId") Integer folderId,
6060
@Param("memberId") Integer memberId);
61+
62+
Optional<Folder> findByArchiveIdAndName(Integer archiveId, String name);
63+
64+
List<Folder> findAllByArchiveId(Integer archiveId);
6165
}

src/main/java/org/tuna/zoopzoop/backend/domain/auth/controller/ApiV1AuthController.java

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import org.springframework.http.HttpStatus;
99
import org.springframework.http.ResponseCookie;
1010
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.core.AuthenticationException;
1112
import org.springframework.web.bind.annotation.*;
12-
import org.springframework.web.reactive.function.client.WebClient;
13-
import org.tuna.zoopzoop.backend.domain.auth.service.KakaoUserInfoService;
13+
import org.tuna.zoopzoop.backend.domain.auth.dto.AuthResultData;
14+
import org.tuna.zoopzoop.backend.domain.auth.entity.AuthResult;
15+
import org.tuna.zoopzoop.backend.domain.auth.entity.RefreshToken;
16+
import org.tuna.zoopzoop.backend.domain.auth.service.RefreshTokenService;
1417
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
15-
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
16-
import org.tuna.zoopzoop.backend.global.config.jwt.JwtProperties;
1718
import org.tuna.zoopzoop.backend.global.rsData.RsData;
1819
import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil;
1920

@@ -23,69 +24,88 @@
2324
@Tag(name = "ApiV1AuthController", description = "인증/인가 REST API 컨트롤러")
2425
public class ApiV1AuthController {
2526
private final JwtUtil jwtUtil;
26-
private final MemberService memberService;
27-
private final JwtProperties jwtProperties;
28-
private final KakaoUserInfoService kakaoUserInfoService;
29-
private final WebClient webClient;
27+
private final RefreshTokenService refreshTokenService;
28+
private final AuthResult authResult;
3029

3130
/**
3231
* 사용자 로그아웃 API
3332
* @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입.
3433
*/
3534
@GetMapping("/logout")
3635
@Operation(summary = "사용자 로그아웃")
37-
public ResponseEntity<RsData<Void>> logout(HttpServletResponse response) {
36+
public ResponseEntity<RsData<Void>> logout(
37+
@CookieValue(name = "sessionId")
38+
String sessionId,
39+
HttpServletResponse response) {
40+
41+
// 서버에서 RefreshToken 삭제
42+
refreshTokenService.deleteBySessionId(sessionId);
43+
44+
// 클라이언트 쿠키 삭제 (AccessToken + SessionId)
3845
ResponseCookie accessCookie = ResponseCookie.from("accessToken", "")
3946
.httpOnly(true)
4047
.path("/")
41-
.maxAge(0) // 쿠키 삭제
48+
.maxAge(0)
4249
.sameSite("Lax")
4350
.build();
4451

45-
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", "")
52+
ResponseCookie sessionCookie = ResponseCookie.from("sessionId", "")
4653
.httpOnly(true)
4754
.path("/")
48-
.maxAge(0) // 쿠키 삭제
55+
.maxAge(0)
4956
.sameSite("Lax")
5057
.build();
5158

5259
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
53-
response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString());
60+
response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString());
5461

5562
return ResponseEntity
5663
.status(HttpStatus.OK)
57-
.body(new RsData<>(
58-
"200",
59-
"정상적으로 로그아웃 했습니다.",
60-
null
61-
)
62-
);
64+
.body(new RsData<>("200", "정상적으로 로그아웃 했습니다.", null));
6365
}
6466

6567
/**
6668
* refreshToken 기반으로 accessToken 재발급
67-
* @param refreshToken 쿠키에 포함된 현재 로그인한 사용자의 refreshToken
69+
* @param sessionId 쿠키에 포함된 현재 로그인한 사용자의 sessionId.
6870
* @param response Servlet 기반 웹에서 server -> client로 http 응답을 보내기 위한 객체, 자동 주입.
6971
*/
7072

7173
@PostMapping("/refresh")
72-
@Operation(summary = "사용자 액세스 토큰 재발급 (리프레시 토큰이 유효할 경우)")
73-
public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshToken", required = false) String refreshToken,
74-
HttpServletResponse response) {
74+
@Operation(summary = "사용자 액세스 토큰 재발급 (서버 저장 RefreshToken 사용)")
75+
public ResponseEntity<RsData<Void>> refreshToken(
76+
@CookieValue(name = "sessionId")
77+
String sessionId,
78+
HttpServletResponse response
79+
) {
7580

76-
if (refreshToken == null || !jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
81+
if (sessionId == null) {
7782
return ResponseEntity
7883
.status(HttpStatus.UNAUTHORIZED)
79-
.body(new RsData<>(
80-
"401",
81-
"유효하지 않은 리프레시 토큰입니다.",
82-
null
83-
));
84+
.body(new RsData<>("401", "세션이 존재하지 않습니다.", null));
8485
}
8586

86-
String providerKey = jwtUtil.getProviderKeyFromToken(refreshToken);
87-
Member member = memberService.findByProviderKey(providerKey);
87+
// sessionId로 RefreshToken 조회
88+
RefreshToken refreshTokenEntity;
89+
try {
90+
refreshTokenEntity = refreshTokenService.getBySessionId(sessionId);
91+
} catch (AuthenticationException e) {
92+
return ResponseEntity
93+
.status(HttpStatus.UNAUTHORIZED)
94+
.body(new RsData<>("401", e.getMessage(), null));
95+
}
96+
97+
String refreshToken = refreshTokenEntity.getRefreshToken();
98+
99+
// RefreshToken 유효성 검사
100+
if (!jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
101+
return ResponseEntity
102+
.status(HttpStatus.UNAUTHORIZED)
103+
.body(new RsData<>("401", "유효하지 않은 리프레시 토큰입니다.", null));
104+
}
88105

106+
Member member = refreshTokenEntity.getMember();
107+
108+
// 새 AccessToken 발급
89109
String newAccessToken = jwtUtil.generateToken(member);
90110

91111
ResponseCookie accessCookie = ResponseCookie.from("accessToken", newAccessToken)
@@ -97,12 +117,38 @@ public ResponseEntity<RsData<Void>> refreshToken(@CookieValue(name = "refreshTok
97117

98118
response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString());
99119

120+
return ResponseEntity
121+
.status(HttpStatus.OK)
122+
.body(new RsData<>("200", "액세스 토큰을 재발급 했습니다.", null));
123+
}
124+
125+
/**
126+
* 확장프로그램의 액세스 토큰 발급을 위한 백그라운드 풀링에 대응하는 API
127+
* @param state 확장프로그램 로그인 시 전달한 state 값.
128+
*/
129+
130+
@GetMapping("/result")
131+
@Operation(summary = "확장프로그램 백그라운드 풀링 대응 API")
132+
public ResponseEntity<RsData<AuthResultData>> pullingResult(
133+
@RequestParam String state
134+
) {
135+
AuthResultData resultData = authResult.get(state);
136+
if(resultData == null) {
137+
return ResponseEntity
138+
.status(HttpStatus.NOT_FOUND)
139+
.body(new RsData<>(
140+
"404",
141+
"state에 해당하는 토큰이 준비되지 않았거나, 잘못된 state 입니다.",
142+
null
143+
)
144+
);
145+
}
100146
return ResponseEntity
101147
.status(HttpStatus.OK)
102148
.body(new RsData<>(
103-
"200",
104-
"액세스 토큰을 재발급 했습니다.",
105-
null
149+
"200",
150+
"토큰이 정상적으로 발급되었습니다.",
151+
resultData
106152
));
107153
}
108154
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.dev.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.context.annotation.Profile;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.web.bind.annotation.*;
7+
import org.springframework.web.server.ResponseStatusException;
8+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
9+
import org.tuna.zoopzoop.backend.domain.member.enums.Provider;
10+
import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository;
11+
import org.tuna.zoopzoop.backend.global.security.jwt.JwtUtil;
12+
13+
import java.util.Map;
14+
15+
@Profile({"local","dev","staging","test"})
16+
@RestController
17+
@RequestMapping("/dev")
18+
@RequiredArgsConstructor
19+
public class DevController {
20+
21+
private final MemberRepository memberRepository;
22+
private final JwtUtil jwtUtil;
23+
24+
@GetMapping("/token")
25+
public Map<String, String> issueToken(
26+
@RequestParam Provider provider,
27+
@RequestParam String key
28+
) {
29+
Member m = memberRepository.findByProviderAndProviderKey(provider, key)
30+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "member not found"));
31+
32+
String accessToken = jwtUtil.generateToken(m);
33+
return Map.of("accessToken", accessToken);
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.dto;
2+
3+
public class AuthResultData {
4+
private final String accessToken;
5+
private final String sessionId;
6+
7+
public AuthResultData(String accessToken, String sessionId) {
8+
this.accessToken = accessToken;
9+
this.sessionId = sessionId;
10+
}
11+
12+
public String getAccessToken() {
13+
return accessToken;
14+
}
15+
16+
public String getSessionId() {
17+
return sessionId;
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.entity;
2+
3+
import org.springframework.stereotype.Component;
4+
import org.tuna.zoopzoop.backend.domain.auth.dto.AuthResultData;
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
@Component
9+
public class AuthResult {
10+
private final Map<String, AuthResultData> results = new ConcurrentHashMap<>();
11+
12+
public void put(String state, String accessToken, String sessionId) {
13+
results.put(state, new AuthResultData(accessToken, sessionId));
14+
}
15+
16+
public AuthResultData get(String state) {
17+
return results.remove(state);
18+
}
19+
20+
public void consume(String state) {
21+
results.remove(state);
22+
}
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.tuna.zoopzoop.backend.domain.auth.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
6+
import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Getter
11+
@Setter
12+
@Entity
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
public class RefreshToken extends BaseEntity {
17+
@OneToOne(fetch = FetchType.LAZY)
18+
@JoinColumn(name = "member_id", unique = true, nullable = false)
19+
private Member member;
20+
21+
@Column(name = "session_id", unique = true, nullable = false)
22+
private String sessionId;
23+
24+
@Column(unique = true, nullable = false)
25+
private String refreshToken;
26+
27+
@Column(name = "created_at", nullable = false, updatable = false)
28+
private LocalDateTime createdAt;
29+
30+
@Column(name = "expired_at")
31+
private LocalDateTime expiredAt;
32+
33+
@PrePersist
34+
public void prePersist() {
35+
if(createdAt == null) {
36+
this.createdAt = LocalDateTime.now();
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)