Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Feat: 영수증 검증 및 구독 상태 조회 API 구현 #219

Merged
merged 16 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
bbb6988
:sparkles: Feat: Subscription Entity 작성
ahnsugyeong Jul 28, 2024
89e3644
:sparkles: Feat: SubscriptionRepository 작성
ahnsugyeong Jul 28, 2024
e2b65ed
:sparkles: Feat: 영수증 검증 및 구독 상태 조회 API 구현
ahnsugyeong Jul 28, 2024
e6538d5
:sparkles: Feat: 구독 상품 검증을 위한 설정 추가
ahnsugyeong Jul 28, 2024
33ba212
:bug: Fix: 로컬에서 JWT 만료되는 버그 해결
ahnsugyeong Aug 4, 2024
3af2ab7
:recycle: Refactor: 영수증 검증 로직 리팩토링
ahnsugyeong Aug 4, 2024
615b36e
:sparkles: Feat: 구독 조회 시 상태 업데이트 로직 추가
ahnsugyeong Aug 4, 2024
8df8ec3
:sparkles: Feat: 스케줄링으로 구독 상태 업데이트 로직 추가
ahnsugyeong Aug 4, 2024
d23556f
:sparkles: Feat: Feign Client 방식에서 SDK 방식으로 변경
ahnsugyeong Aug 4, 2024
abacf12
:sparkles: Feat: 결제 정보 검증 로직 보완
ahnsugyeong Aug 5, 2024
846ef5c
:sparkles: Feat: 사용자 인증 로직 구현
ahnsugyeong Aug 5, 2024
3e049db
:recycle: Refactor: 트랜잭션 범위 최소화를 위한 외부 API 호출과 DB 작업 분리
ahnsugyeong Aug 9, 2024
f664afe
:recycle: Refactor: 구독 상태 확인 로직을 JPA exists 쿼리로 변경
ahnsugyeong Aug 9, 2024
c549798
:recycle: Refactor: 회원 구독 정보 조회 방식을 DB 조회로 최적화
ahnsugyeong Aug 9, 2024
156b740
:recycle: Refactor: Subscription API 기본 경로 통합
ahnsugyeong Aug 9, 2024
a1445ba
:bug: Fix: ErrorCode 오타 수정
ahnsugyeong Aug 9, 2024
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
6 changes: 6 additions & 0 deletions Briefing-Api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ dependencies {

//spring batch 의존성
implementation 'org.springframework.boot:spring-boot-starter-batch'

// google play
implementation "com.google.api-client:google-api-client:1.33.0"
implementation 'com.google.auth:google-auth-library-oauth2-http:1.6.0'
implementation 'com.google.apis:google-api-services-androidpublisher:v3-rev20220411-1.32.1'
implementation 'com.google.http-client:google-http-client-jackson2:1.41.7'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;


@OpenAPIDefinition(
Expand All @@ -20,6 +21,7 @@
})
@SpringBootApplication(scanBasePackages = {"com.example.briefingapi","com.example.briefingcommon","com.example.briefinginfra"})
@RequiredArgsConstructor
@EnableScheduling
@EnableCaching
@EnableFeignClients(basePackages = "com.example.briefinginfra")
@EnableRedisRepositories
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.briefingapi.config;

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;

@Component
public class GoogleCredentialsConfig {

@Value("${subscription.google.keyfile.content}")
private String googleAccountFileContent;

public AndroidPublisher androidPublisher() throws IOException, GeneralSecurityException {
InputStream inputStream = new ByteArrayInputStream(googleAccountFileContent.getBytes());
GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream)
.createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);

JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

return new AndroidPublisher.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
jsonFactory,
new HttpCredentialsAdapter(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.example.briefingapi.member.business;

import java.util.List;

import com.example.briefingapi.fcm.implementation.FcmCommandService;
import com.example.briefingapi.member.implement.MemberCommandAdapter;
import com.example.briefingapi.member.implement.MemberQueryAdapter;
import com.example.briefingapi.member.presentation.dto.MemberRequest;
import com.example.briefingapi.member.presentation.dto.MemberResponse;
import com.example.briefingapi.redis.service.RedisService;
import com.example.briefingapi.security.provider.TokenProvider;
import com.example.briefingcommon.entity.Member;
import com.example.briefingcommon.entity.enums.MemberRole;
Expand All @@ -15,16 +14,13 @@
import com.example.briefinginfra.feign.oauth.apple.client.AppleOauth2Client;
import com.example.briefinginfra.feign.oauth.google.client.GoogleOauth2Client;
import com.example.briefinginfra.feign.oauth.google.dto.GoogleUserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import com.example.briefingapi.redis.service.RedisService;

import lombok.RequiredArgsConstructor;
import java.util.List;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -79,10 +75,10 @@ public MemberResponse.TestTokenDTO getTestToken(){
return MemberResponse.TestTokenDTO.builder()
.token(
tokenProvider.createAccessToken(
member.getId(),
member.getSocialType().toString(),
member.getSocialId(),
List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))))
member.getId(),
member.getSocialType().toString(),
member.getSocialId(),
List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name()))))
.refeshToken(redisService.generateTestRefreshToken())
.build();
}
Expand Down Expand Up @@ -116,4 +112,4 @@ public void subScribeDailyPush(MemberRequest.ToggleDailyPushAlarmDTO request, Me
fcmCommandService.unSubScribe(dailyPushTopic,request.getFcmToken());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.example.briefingapi.security.handler.annotation;

import io.swagger.v3.oas.annotations.Parameter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Parameter(hidden = true)
public @interface AuthMember {}
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package com.example.briefingapi.security.provider;
import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

import com.example.briefingapi.exception.JwtAuthenticationException;
import com.example.briefingcommon.common.exception.common.ErrorCode;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
Expand All @@ -22,8 +17,14 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements InitializingBean {
Expand All @@ -38,8 +39,6 @@ public class TokenProvider implements InitializingBean {

private final long accessTokenValidityInMilliseconds;

// private final RefreshTokenRepository refreshTokenRepository;

private Key key;

public enum TokenType {
Expand All @@ -50,12 +49,10 @@ public enum TokenType {
public TokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.authorities-key}") String authoritiesKey,
@Value("${jwt.access-token-validity-in-seconds}")
long accessTokenValidityInMilliseconds) {
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds) {
this.secret = secret;
this.AUTHORITIES_KEY = authoritiesKey;
this.accessTokenValidityInMilliseconds = accessTokenValidityInMilliseconds;
// this.refreshTokenRepository = refreshTokenRepository;
this.accessTokenValidityInMilliseconds = accessTokenValidityInSeconds * 1000;
}

@Override
Expand All @@ -64,82 +61,51 @@ public void afterPropertiesSet() throws Exception {
this.key = Keys.hmacShaKeyFor(keyBytes);
}

// 수정 해야함
public String createAccessToken(
Long userId,
String socialType,
String socialId,
Collection<? extends GrantedAuthority> authorities) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.accessTokenValidityInMilliseconds);
public String createAccessToken(Long userId, String socialType, String socialId, Collection<? extends GrantedAuthority> authorities) {
Instant issuedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS);
Instant expiration = issuedAt.plus(accessTokenValidityInMilliseconds, ChronoUnit.MILLIS);

return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim(AUTHORITIES_KEY, authorities)
.claim(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")))
.claim("socialType", socialType)
.claim("socialID", socialId)
.setIssuedAt(Date.from(issuedAt))
.setExpiration(Date.from(expiration))
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}

public String createAccessToken(
Long userId, String phoneNum, Collection<? extends GrantedAuthority> authorities) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.accessTokenValidityInMilliseconds);

return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim(AUTHORITIES_KEY, authorities)
.claim("phoneNum", phoneNum)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}

public Authentication getAuthentication(String token) {
Claims claims =
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

public boolean validateToken(String token, TokenType type) throws JwtAuthenticationException {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
LOGGER.info("JWT Token is valid. Expiration: {}", claimsJws.getBody().getExpiration());
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
LOGGER.error("Invalid JWT signature: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
} catch (ExpiredJwtException e) {
if (type == TokenType.ACCESS)
throw new JwtAuthenticationException(ErrorCode.EXPIRED_JWT_EXCEPTION);
else throw new JwtAuthenticationException(ErrorCode.RELOGIN_EXCEPTION);
LOGGER.warn("Expired JWT token: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.EXPIRED_JWT_EXCEPTION);
} catch (UnsupportedJwtException e) {
LOGGER.error("Unsupported JWT token: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
} catch (IllegalArgumentException e) {
LOGGER.error("JWT token compact of handler are invalid: {}", e.getMessage());
throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN_EXCEPTION);
}
}

// public Long validateAndReturnId(String token) throws JwtAuthenticationException{
// try{
// Claims body =
// Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
// return Long.valueOf(body.getSubject());
// }catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e){
// throw new JwtAuthenticationException(Er.JWT_BAD_REQUEST);
// }catch (UnsupportedJwtException e){
// throw new JwtAuthenticationException(Code.JWT_UNSUPPORTED_TOKEN);
// }catch (IllegalArgumentException e){
// throw new JwtAuthenticationException(Code.JWT_BAD_REQUEST);
// }
// }

public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.briefingapi.subscription.business;

import com.example.briefingapi.subscription.presentation.dto.SubscriptionRequest;
import com.example.briefingapi.subscription.presentation.dto.SubscriptionResponse;
import com.example.briefingcommon.entity.Member;
import com.example.briefingcommon.entity.Subscription;
import com.example.briefingcommon.entity.enums.SubscriptionStatus;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SubscriptionMapper {

public static Subscription toSubscription(Member member, SubscriptionRequest.ReceiptDTO request, LocalDateTime expiryDate) {
return Subscription.builder()
.member(member)
.type(request.getSubscriptionType())
.status(LocalDateTime.now().isBefore(expiryDate) ? SubscriptionStatus.ACTIVE : SubscriptionStatus.EXPIRED)
.expiryDate(expiryDate)
.build();
}

public static SubscriptionResponse.SubscriptionDTO toSubscriptionDTO(Subscription subscription) {
return SubscriptionResponse.SubscriptionDTO.builder()
.id(subscription.getId())
.memberId(subscription.getMember().getId())
.type(subscription.getType())
.status(subscription.getStatus())
.expiryDate(subscription.getExpiryDate())
.build();
}

}
Loading