Skip to content

Commit af3720e

Browse files
authored
Merge pull request #169 from UMC-Plantory/develop
[Main Merge] Plantory-V1
2 parents 8d9c35e + 25f2ee0 commit af3720e

58 files changed

Lines changed: 1047 additions & 57 deletions

File tree

Some content is hidden

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

.github/workflows/dev_deploy.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ jobs:
2424
echo "${{ secrets.APPLICATION_YML }}" > ./application.yml
2525
shell: bash
2626

27+
- name: Create Firebase JSON # Firebase 서비스 계정 JSON 생성
28+
run: |
29+
mkdir -p src/main/resources/security/server-security/fcm
30+
echo "${{ secrets.FIREBASE_JSON }}" > src/main/resources/security/server-security/fcm/plantory-firebase-adminsdk.json
31+
shell: bash
32+
33+
- name: Create Apple Auth Key # apple auth key 생성
34+
run: |
35+
mkdir -p src/main/resources/security/server-security/apple
36+
echo "${{ secrets.APPLE_AUTH_KEY_P8 }}" > src/main/resources/security/server-security/apple/plantory-auth-key.p8
37+
shell: bash
38+
2739
- name: Grant execute permission for gradlew # gradlew 실행 권한 부여
2840
run: chmod +x gradlew
2941

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ out/
3838
.vscode/
3939

4040
/src/main/resources/*.yml
41-
.DS_Store
41+
.DS_Store
42+
/src/main/resources/security/Server-Security/fcm/plantory-firebase-adminsdk.json
43+
/src/main/resources/security/Server-Security/apple/plantory-auth-key.p8

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ dependencies {
7171
// JTokkit
7272
implementation 'com.knuddels:jtokkit:1.1.0'
7373

74+
// firebase
75+
implementation "com.google.firebase:firebase-admin:9.3.0"
76+
7477
}
7578

7679
tasks.named('test') {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package umc.plantory.domain.apple.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
import org.hibernate.annotations.ColumnDefault;
6+
import org.hibernate.annotations.DynamicInsert;
7+
import org.hibernate.annotations.DynamicUpdate;
8+
import umc.plantory.global.baseEntity.BaseEntity;
9+
10+
@Entity
11+
@Builder
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
@AllArgsConstructor
14+
@Getter
15+
@DynamicInsert
16+
@DynamicUpdate
17+
public class AppleAuthData extends BaseEntity {
18+
@Id
19+
@GeneratedValue(strategy = GenerationType.IDENTITY)
20+
@Column(name = "apple_auth_data_id")
21+
private Long id;
22+
23+
@Column(length = 255)
24+
private String clientSecret;
25+
26+
@Column(length = 10, nullable = false)
27+
@ColumnDefault("'plantory'")
28+
private String tag;
29+
30+
public void updateClientSecret(String newClientSecret) {
31+
this.clientSecret = newClientSecret;
32+
}
33+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package umc.plantory.domain.apple.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import umc.plantory.domain.apple.entity.AppleAuthData;
5+
6+
import java.util.Optional;
7+
8+
public interface AppleAuthDataRepository extends JpaRepository<AppleAuthData, Long> {
9+
Optional<AppleAuthData> findByTag (String tag);
10+
}

src/main/java/umc/plantory/domain/apple/sevice/AppleOidcService.java

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,40 @@
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import io.jsonwebtoken.Claims;
6-
import io.jsonwebtoken.ExpiredJwtException;
7-
import io.jsonwebtoken.JwtParser;
8-
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.*;
96
import lombok.RequiredArgsConstructor;
107
import lombok.extern.slf4j.Slf4j;
118
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.core.io.Resource;
1210
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
import org.springframework.web.reactive.function.BodyInserters;
13+
import org.springframework.web.reactive.function.client.WebClient;
1314
import umc.plantory.domain.apple.converter.AppleConverter;
1415
import umc.plantory.domain.member.dto.MemberDataDTO;
1516
import umc.plantory.domain.member.dto.MemberRequestDTO;
1617
import umc.plantory.global.apiPayload.code.status.ErrorStatus;
1718
import umc.plantory.global.apiPayload.exception.handler.AppleHandler;
1819

20+
import java.io.BufferedReader;
21+
import java.io.IOException;
22+
import java.io.InputStreamReader;
1923
import java.math.BigInteger;
24+
import java.nio.charset.StandardCharsets;
2025
import java.security.KeyFactory;
26+
import java.security.NoSuchAlgorithmException;
27+
import java.security.PrivateKey;
28+
import java.security.interfaces.ECPrivateKey;
2129
import java.security.interfaces.RSAPublicKey;
30+
import java.security.spec.InvalidKeySpecException;
31+
import java.security.spec.PKCS8EncodedKeySpec;
2232
import java.security.spec.RSAPublicKeySpec;
33+
import java.time.Instant;
2334
import java.util.Base64;
2435
import java.net.URL;
36+
import java.util.Date;
2537
import java.util.List;
38+
import java.util.stream.Collectors;
2639

2740
@Slf4j
2841
@Service
@@ -33,8 +46,20 @@ public class AppleOidcService {
3346
private static final String JWK_URL = "https://appleid.apple.com/auth/keys";
3447
// iss 검증 값
3548
private static final String ISSUER = "https://appleid.apple.com";
49+
// 약 30일
50+
private static final long MAX_EXP_SECONDS = 60L * 60L * 24L * 30L;
51+
52+
private final WebClient webClient;
53+
3654
@Value("${apple.bundle-id}")
3755
private String BUNDLE_ID;
56+
@Value("${apple.p8.location:}")
57+
private Resource p8Resource;
58+
@Value("${apple.key-id}")
59+
private String KEY_ID;
60+
@Value("${apple.team-id}")
61+
private String TEAM_ID;
62+
3863

3964
/**
4065
* identity_token을 검증하고 담겨있는 멤버 데이터 추출
@@ -97,4 +122,89 @@ public MemberDataDTO.MemberData verifyAndParseIdToken(MemberRequestDTO.AppleOAut
97122
throw new AppleHandler(ErrorStatus.ERROR_ON_VERIFYING);
98123
}
99124
}
125+
126+
/**
127+
* Authorization Code 를 통해 apple refresh_token 값을 받아오는 메서드
128+
*/
129+
public String createAppleRefreshToken(String authorizationCode, String clientSecret) {
130+
return webClient.post()
131+
.uri("https://appleid.apple.com/auth/token")
132+
.header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
133+
.body(BodyInserters.fromFormData("grant_type", "authorization_code")
134+
.with("code", authorizationCode)
135+
.with("client_id", BUNDLE_ID)
136+
.with("client_secret", clientSecret))
137+
.retrieve()
138+
.bodyToMono(String.class)
139+
.doOnNext(System.out::println)
140+
.block();
141+
}
142+
143+
/**
144+
* Apple Client_Secret 생성 메서드
145+
*/
146+
public String createAppleClientSecret() {
147+
try {
148+
ECPrivateKey privateKey = (ECPrivateKey) loadPrivateKeyFromPem();
149+
150+
Date iat = Date.from(Instant.now());
151+
Date exp = Date.from(Instant.now().plusSeconds(MAX_EXP_SECONDS));
152+
153+
String jwt = Jwts.builder()
154+
.setHeaderParam("kid", KEY_ID)
155+
.setIssuer(TEAM_ID)
156+
.setSubject(BUNDLE_ID)
157+
.setAudience(ISSUER)
158+
.setIssuedAt(iat)
159+
.setExpiration(exp)
160+
.signWith(privateKey, SignatureAlgorithm.ES256)
161+
.compact();
162+
163+
return jwt;
164+
} catch (Exception e) {
165+
log.error("Failed to Refresh Apple Client_Secret", e);
166+
throw new RuntimeException("Failed to Refresh Apple Client_Secret", e);
167+
}
168+
}
169+
170+
/**
171+
* pem 파일에 적힌 privateKey 읽어오는 메서드
172+
*/
173+
private PrivateKey loadPrivateKeyFromPem() throws Exception {
174+
String pem;
175+
if (p8Resource != null && p8Resource.exists()) {
176+
try (BufferedReader br = new BufferedReader(new InputStreamReader(p8Resource.getInputStream(), StandardCharsets.UTF_8))) {
177+
pem = br.lines().collect(Collectors.joining("\n"));
178+
}
179+
} else {
180+
throw new AppleHandler(ErrorStatus._INTERNAL_SERVER_ERROR);
181+
}
182+
183+
String normalized = pem
184+
.replace("-----BEGIN PRIVATE KEY-----", "")
185+
.replace("-----END PRIVATE KEY-----", "")
186+
.replaceAll("\\s", "");
187+
188+
byte[] der = Base64.getDecoder().decode(normalized);
189+
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(der);
190+
KeyFactory kf = KeyFactory.getInstance("EC");
191+
return kf.generatePrivate(keySpec);
192+
}
193+
194+
/**
195+
* 플랜토리 - 애플 연동 해제 메서드
196+
*/
197+
public void unlinkUser(String refreshToken, String clientSecret) {
198+
webClient.post()
199+
.uri("https://appleid.apple.com/auth/revoke")
200+
.header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
201+
.body(BodyInserters.fromFormData("token_type_hint", "refresh_token")
202+
.with("token", refreshToken)
203+
.with("client_id", BUNDLE_ID)
204+
.with("client_secret", clientSecret))
205+
.retrieve()
206+
.bodyToMono(String.class)
207+
.doOnNext(System.out::println)
208+
.block();
209+
}
100210
}

src/main/java/umc/plantory/domain/chat/controller/ChatRestController.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,37 @@ public ResponseEntity<ApiResponse<ChatResponseDTO.ChatsResponse>> getChats(
6969
ChatResponseDTO.ChatsResponse chatList = chatQueryUseCase.findChatList(authorization, cursor, size);
7070
return ResponseEntity.ok(ApiResponse.onSuccess(chatList));
7171
}
72+
73+
@DeleteMapping
74+
@Operation(
75+
summary = "채팅창 초기화",
76+
description = "지금까지의 채팅 내역을 초기화"
77+
)
78+
@ApiResponses({
79+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "성공"),
80+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "MEMBER4001", description = "존재하지 않는 회원입니다", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
81+
})
82+
public ResponseEntity<ApiResponse<Void>> deleteChats(
83+
@RequestHeader(value = "Authorization", required = false) String authorization
84+
) {
85+
chatCommandUseCase.delete(authorization);
86+
return ResponseEntity.ok(ApiResponse.onSuccess(null));
87+
};
88+
89+
@GetMapping("/search")
90+
@Operation(
91+
summary = "채팅 검색",
92+
description = "입력한 단어의 채팅 검색")
93+
@ApiResponses({
94+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "성공"),
95+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "MEMBER4001", description = "존재하지 않는 회원입니다", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
96+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CHAT4008", description = "해당 키워드를 포함하는 채팅이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
97+
})
98+
public ResponseEntity<ApiResponse<ChatResponseDTO.ChatIdsResponse>> searchChatIdsByKeyword(
99+
@RequestHeader(value = "Authorization", required = false) String authorization,
100+
@RequestParam(value = "keyword") String keyword
101+
) {
102+
ChatResponseDTO.ChatIdsResponse result = chatQueryUseCase.searchChatIdsByKeyword(authorization, keyword);
103+
return ResponseEntity.ok(ApiResponse.onSuccess(result));
104+
}
72105
}

src/main/java/umc/plantory/domain/chat/converter/ChatConverter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ public static ChatResponseDTO.ChatResponse toChatResponseDTO(String response, Lo
4545
.isMember(isMember)
4646
.build();
4747
}
48+
49+
public static ChatResponseDTO.ChatIdsResponse toChatIdsResponse(List<Long> chatIdList) {
50+
return ChatResponseDTO.ChatIdsResponse.builder()
51+
.chatIdList(chatIdList)
52+
.build();
53+
}
4854
}

src/main/java/umc/plantory/domain/chat/dto/ChatResponseDTO.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,13 @@ public static class ChatResponse {
5050
@Schema(description = "사용자 요청인지 챗봇 응답인지", example = "false")
5151
private Boolean isMember;
5252
}
53+
54+
@Getter
55+
@Builder
56+
@NoArgsConstructor
57+
@AllArgsConstructor
58+
public static class ChatIdsResponse {
59+
@Schema(description = "채팅 아이디", example = "[14, 13, 12]")
60+
private List<Long> chatIdList;
61+
}
5362
}

src/main/java/umc/plantory/domain/chat/repository/ChatRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
import org.springframework.data.jpa.repository.JpaRepository;
44
import umc.plantory.domain.chat.entity.Chat;
5+
import umc.plantory.domain.member.entity.Member;
6+
7+
import java.util.List;
58

69
public interface ChatRepository extends JpaRepository<Chat, Long> {
10+
void deleteByMember(Member member);
11+
void deleteAllByMember(Member member);
12+
List<Chat> findByMemberAndContentContainingIgnoreCaseOrderByCreatedAtDesc(Member member, String keyword);
713
}

0 commit comments

Comments
 (0)