diff --git a/Dockerfile b/Dockerfile index ccbf8350..99419748 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ -FROM openjdk:17 +FROM openjdk:17-alpine COPY build/libs/team-c-back-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 0870465f..561fdc63 100644 --- a/build.gradle +++ b/build.gradle @@ -126,6 +126,10 @@ dependencies { // 크롤링 implementation 'org.jsoup:jsoup:1.17.2' + + // Apple App Store Server Library + implementation 'com.apple.itunes.storekit:app-store-server-library:3.6.0' + } dependencyManagement { diff --git a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java index b5ffead9..a9c17112 100644 --- a/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java +++ b/src/main/java/devkor/com/teamcback/domain/operatingtime/scheduler/OperatingScheduler.java @@ -30,7 +30,7 @@ public class OperatingScheduler { private static Boolean isEvenWeek = null; @Scheduled(cron = "0 0 0 * * *") // 매일 자정마다 - @EventListener(ApplicationReadyEvent.class) + // @EventListener(ApplicationReadyEvent.class) public void updateOperatingTime() { setState(); log.info("운영 시간 업데이트"); diff --git a/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java b/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java new file mode 100644 index 00000000..e9166b91 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/controller/AppleController.java @@ -0,0 +1,32 @@ +package devkor.com.teamcback.domain.user.controller; + +import com.apple.itunes.storekit.verification.VerificationException; +import devkor.com.teamcback.domain.user.dto.request.AppleNotationReq; +import devkor.com.teamcback.domain.user.dto.response.AppleNotificationRes; +import devkor.com.teamcback.domain.user.service.AppleService; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.response.ResultCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/apple") +public class AppleController { + + private final AppleService appleService; + + /** + * Apple 로그인 계정 상태 알림을 수신하는 엔드포인트 + */ + @PostMapping("/notifications") + public CommonResponse handleAppleNotification(@RequestBody AppleNotationReq request) throws VerificationException { + + return CommonResponse.success(appleService.handleAppleNotification(request)); + + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java b/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java new file mode 100644 index 00000000..e9805d54 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/dto/request/AppleNotationReq.java @@ -0,0 +1,10 @@ +package devkor.com.teamcback.domain.user.dto.request; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AppleNotationReq { + private String signedPayload; +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java b/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java new file mode 100644 index 00000000..59e6ee4b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/dto/response/AppleNotificationRes.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.user.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties +public class AppleNotificationRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java b/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java new file mode 100644 index 00000000..9bffb55b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/user/service/AppleService.java @@ -0,0 +1,97 @@ +package devkor.com.teamcback.domain.user.service; + +import com.apple.itunes.storekit.model.Environment; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload; +import com.apple.itunes.storekit.verification.SignedDataVerifier; +import com.apple.itunes.storekit.verification.VerificationException; +import devkor.com.teamcback.domain.user.dto.request.AppleNotationReq; +import devkor.com.teamcback.domain.user.dto.response.AppleNotificationRes; +import devkor.com.teamcback.domain.user.repository.UserRepository; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.ResultCode; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +@Service +public class AppleService { + + @Autowired + private UserRepository userRepository; + private SignedDataVerifier verifier; + private static final String BUNDLE_ID = "com.devkor.kodaero"; + + @PostConstruct + public void init() throws FileNotFoundException { + + InputStream certInputStream = getClass().getClassLoader().getResourceAsStream("static/apple/AppleRootCA-G3.cer"); + + Set rootCertificates = new HashSet<>(); + rootCertificates.add(certInputStream); + + this.verifier = new SignedDataVerifier( + rootCertificates, + BUNDLE_ID, + null, + Environment.LOCAL_TESTING, + // Environment.PRODUCTION, + false + ); + } + + + @Transactional + public AppleNotificationRes handleAppleNotification(AppleNotationReq request) { + + String signedPayload = request.getSignedPayload(); + + if (signedPayload == null || signedPayload.isEmpty()) { + throw new GlobalException(ResultCode.INVALID_INPUT); + } + + try { + ResponseBodyV2DecodedPayload payload = verifier.verifyAndDecodeNotification(signedPayload); + + String notificationType = payload.getNotificationType().toString(); + String uuid = payload.getNotificationUUID(); + + System.out.println("알림 유형: " + notificationType); + System.out.println("UUID: " + uuid); + + String signedTransactionInfo = payload.getData().getSignedTransactionInfo(); + String signedRenewalInfo = payload.getData().getSignedRenewalInfo(); + + JWSTransactionDecodedPayload transactionInfo = verifier.verifyAndDecodeTransaction(signedTransactionInfo); + String originalTransactionId = transactionInfo.getOriginalTransactionId(); + + // 내부 사용자 ID 조회 + // String userId = userRepository.findUserIdByOriginalTransactionId(originalTransactionId); + + switch (notificationType) { + case "account_delete": + break; + case "email_change": + break; + default: + System.out.println("Unhandled Apple notification: " + notificationType); + break; + } + + return new AppleNotificationRes(); + + } catch (Exception e) { + System.err.println("Apple SiWA Notification processing failed: " + e.getMessage()); + throw new GlobalException(ResultCode.SYSTEM_ERROR); + } + } + +} diff --git a/src/main/resources/static/apple/AppleRootCA-G3.cer b/src/main/resources/static/apple/AppleRootCA-G3.cer new file mode 100644 index 00000000..228bfa39 Binary files /dev/null and b/src/main/resources/static/apple/AppleRootCA-G3.cer differ