diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthController.java new file mode 100644 index 00000000..328d2c3e --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthController.java @@ -0,0 +1,66 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.RefreshTokenReqDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.TokenReqDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.MemberLoginResDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; +import shop.kkeujeok.kkeujeokbackend.auth.application.AuthMemberService; +import shop.kkeujeok.kkeujeokbackend.auth.application.AuthService; +import shop.kkeujeok.kkeujeokbackend.auth.application.AuthServiceFactory; +import shop.kkeujeok.kkeujeokbackend.auth.application.TokenService; +import shop.kkeujeok.kkeujeokbackend.global.jwt.api.dto.TokenDto; +import shop.kkeujeok.kkeujeokbackend.global.oauth.GoogleAuthService; +import shop.kkeujeok.kkeujeokbackend.global.oauth.KakaoAuthService; +import shop.kkeujeok.kkeujeokbackend.global.template.RspTemplate; +import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; + +@RestController +@Slf4j +@RequestMapping("/api") +@RequiredArgsConstructor +public class AuthController { + private final AuthServiceFactory authServiceFactory; + private final AuthMemberService memberService; + private final TokenService tokenService; + private final GoogleAuthService getGoogleAccessToken; + private final KakaoAuthService kakaoAuthService; + + @GetMapping("oauth2/callback/google") + public JsonNode googleCallback(@RequestParam(name = "code") String code) { + return getGoogleAccessToken.getGoogleIdToken(code); + } + + @GetMapping("oauth2/callback/kakao") + public JsonNode kakaoCallback(@RequestParam(name = "code") String code) { + return kakaoAuthService.getKakaoAccessToken(code); + } + +// @Operation(summary = "로그인 후 토큰 발급", description = "액세스, 리프레쉬 토큰을 발급합니다.") + @PostMapping("/{provider}/token") + public RspTemplate generateAccessAndRefreshToken( + @PathVariable(name = "provider") String provider, + @RequestBody TokenReqDto tokenReqDto) { + AuthService authService = authServiceFactory.getAuthService(provider); + UserInfo userInfo = authService.getUserInfo(tokenReqDto.authCode()); + + MemberLoginResDto getMemberDto = memberService.saveUserInfo(userInfo, + SocialType.valueOf(provider.toUpperCase())); + TokenDto getToken = tokenService.getToken(getMemberDto); + + return new RspTemplate<>(HttpStatus.OK, "토큰 발급", getToken); + } + +// @Operation(summary = "액세스 토큰 재발급", description = "리프레쉬 토큰으로 액세스 토큰을 발급합니다.") + @PostMapping("/token/access") + public RspTemplate generateAccessToken(@RequestBody RefreshTokenReqDto refreshTokenReqDto) { + TokenDto getToken = tokenService.generateAccessToken(refreshTokenReqDto); + + return new RspTemplate<>(HttpStatus.OK, "액세스 토큰 발급", getToken); + } + +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/RefreshTokenReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/RefreshTokenReqDto.java new file mode 100644 index 00000000..9f67ad58 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/RefreshTokenReqDto.java @@ -0,0 +1,6 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api.dto.request; + +public record RefreshTokenReqDto( + String refreshToken +){ +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/TokenReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/TokenReqDto.java new file mode 100644 index 00000000..42a86934 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/request/TokenReqDto.java @@ -0,0 +1,6 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api.dto.request; + +public record TokenReqDto( + String authCode +) { +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/AccessAndRefreshTokenResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/AccessAndRefreshTokenResDto.java new file mode 100644 index 00000000..b6e375b8 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/AccessAndRefreshTokenResDto.java @@ -0,0 +1,10 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api.dto.response; + +import lombok.Builder; + +@Builder +public record AccessAndRefreshTokenResDto( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/MemberLoginResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/MemberLoginResDto.java new file mode 100644 index 00000000..f8073197 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/MemberLoginResDto.java @@ -0,0 +1,16 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api.dto.response; + + +import lombok.Builder; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +@Builder +public record MemberLoginResDto( + Member findMember +) { + public static MemberLoginResDto from(Member member) { + return MemberLoginResDto.builder() + .findMember(member) + .build(); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/UserInfo.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/UserInfo.java new file mode 100644 index 00000000..edd7b097 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/api/dto/response/UserInfo.java @@ -0,0 +1,9 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api.dto.response; + +public record UserInfo( + String email, + String name, + String picture, + String nickname +) { +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberService.java new file mode 100644 index 00000000..707e90e3 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberService.java @@ -0,0 +1,85 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.MemberLoginResDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; +import shop.kkeujeok.kkeujeokbackend.global.entity.Status; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; +import shop.kkeujeok.kkeujeokbackend.member.domain.Role; +import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; +import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; + +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +public class AuthMemberService { + private final MemberRepository memberRepository; + + public AuthMemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Transactional + public MemberLoginResDto saveUserInfo(UserInfo userInfo, SocialType provider) { + validateNotFoundEmail(userInfo.email()); + + Member member = getExistingMemberOrCreateNew(userInfo, provider); + + validateSocialType(member, provider); + + return MemberLoginResDto.from(member); + } + + private void validateNotFoundEmail(String email) { + if (email == null) { + throw new RuntimeException(); + } + } + + private Member getExistingMemberOrCreateNew(UserInfo userInfo, SocialType provider) { + return memberRepository.findByEmail(userInfo.email()).orElseGet(() -> createMember(userInfo, provider)); + } + + private Member createMember(UserInfo userInfo, SocialType provider) { + String userPicture = getUserPicture(userInfo.picture()); + String name = userInfo.name(); + String nickname = userInfo.nickname(); + + if (name == null && nickname != null) { + name = nickname; + } else if (nickname == null && name != null) { + nickname = name; + } + + return memberRepository.save( + Member.builder() + .status(Status.A) + .email(userInfo.email()) + .name(name) + .picture(userPicture) + .socialType(provider) + .role(Role.ROLE_USER) + .firstLogin(true) + .nickname(nickname) + .build() + ); + } + + private String getUserPicture(String picture) { + return Optional.ofNullable(picture) + .map(this::convertToHighRes).orElseThrow(); + } + + private String convertToHighRes(String url){ + return url.replace("s96-c", "s2048-c"); + } + + private void validateSocialType(Member member, SocialType provider) { + if (!provider.equals(member.getSocialType())) { + throw new RuntimeException(); + } + } + +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthService.java new file mode 100644 index 00000000..06e4220e --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthService.java @@ -0,0 +1,9 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; + +public interface AuthService { + UserInfo getUserInfo(String authCode); + + String getProvider(); +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactory.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactory.java new file mode 100644 index 00000000..a6a34533 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactory.java @@ -0,0 +1,25 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class AuthServiceFactory { + private final Map authServiceMap; + + @Autowired + public AuthServiceFactory(List authServiceList) { + authServiceMap = new HashMap<>(); + for (AuthService authService : authServiceList) { + authServiceMap.put(authService.getProvider(), authService); + } + } + + public AuthService getAuthService(String provider) { + return authServiceMap.get(provider); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenService.java new file mode 100644 index 00000000..4a67bd8f --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenService.java @@ -0,0 +1,65 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.RefreshTokenReqDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.MemberLoginResDto; +import shop.kkeujeok.kkeujeokbackend.global.jwt.TokenProvider; +import shop.kkeujeok.kkeujeokbackend.global.jwt.api.dto.TokenDto; +import shop.kkeujeok.kkeujeokbackend.global.jwt.domain.Token; +import shop.kkeujeok.kkeujeokbackend.global.jwt.domain.repository.TokenRepository; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; +import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; + +@Service +@Transactional(readOnly = true) +public class TokenService { + + private final TokenProvider tokenProvider; + private final TokenRepository tokenRepository; + private final MemberRepository memberRepository; + + public TokenService(TokenProvider tokenProvider, TokenRepository tokenRepository, MemberRepository memberRepository) { + this.tokenProvider = tokenProvider; + this.tokenRepository = tokenRepository; + this.memberRepository = memberRepository; + } + + @Transactional + public TokenDto getToken(MemberLoginResDto memberLoginResDto) { + TokenDto tokenDto = tokenProvider.generateToken(memberLoginResDto.findMember().getEmail()); + + tokenSaveAndUpdate(memberLoginResDto, tokenDto); + + return tokenDto; + } + + private void tokenSaveAndUpdate(MemberLoginResDto memberLoginResDto, TokenDto tokenDto) { + if (!tokenRepository.existsByMember(memberLoginResDto.findMember())) { + tokenRepository.save(Token.builder() + .member(memberLoginResDto.findMember()) + .refreshToken(tokenDto.refreshToken()) + .build()); + } + + refreshTokenUpdate(memberLoginResDto, tokenDto); + } + + private void refreshTokenUpdate(MemberLoginResDto memberLoginResDto, TokenDto tokenDto) { + Token token = tokenRepository.findByMember(memberLoginResDto.findMember()).orElseThrow(); + token.refreshTokenUpdate(tokenDto.refreshToken()); + } + + @Transactional + public TokenDto generateAccessToken(RefreshTokenReqDto refreshTokenReqDto) { + if (!tokenRepository.existsByRefreshToken(refreshTokenReqDto.refreshToken()) || !tokenProvider.validateToken(refreshTokenReqDto.refreshToken())) { + throw new RuntimeException(); + } + + Token token = tokenRepository.findByRefreshToken(refreshTokenReqDto.refreshToken()).orElseThrow(); + Member member = memberRepository.findById(token.getMember().getId()).orElseThrow(); + + return tokenProvider.generateAccessTokenByRefreshToken(member.getEmail(), token.getRefreshToken()); + } + +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/Token.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/Token.java index fcb7a27a..a9b43785 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/Token.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/Token.java @@ -24,7 +24,7 @@ public class Token { private String refreshToken; @Builder - private Token(Member member, String refreshToken) { + public Token(Member member, String refreshToken) { this.member = member; this.refreshToken = refreshToken; } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthService.java new file mode 100644 index 00000000..00895eaa --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthService.java @@ -0,0 +1,99 @@ +package shop.kkeujeok.kkeujeokbackend.global.oauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; +import shop.kkeujeok.kkeujeokbackend.auth.application.AuthService; +import shop.kkeujeok.kkeujeokbackend.global.oauth.exception.OAuthException; +import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +@Service +@Transactional(readOnly = true) +public class GoogleAuthService implements AuthService { + + private final String GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; + @Value("${client_id}") + private String google_client_id; + @Value("${client_secret}") + private String google_client_secret; + + @Value("${GOOGLE_REDIRECT_URI}") + private String GOOGLE_REDIRECT_URI; + private static final String JWT_DELIMITER = "\\."; + + private final ObjectMapper objectMapper; + + public GoogleAuthService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + + public JsonNode getGoogleIdToken(String code) { + RestTemplate restTemplate = new RestTemplate(); + Map params = Map.of( + "code", code, + "scope", "https://www.googleapis.com/auth/userinfo.profile " + + "https://www.googleapis.com/auth/userinfo.email", + "client_id", google_client_id, + "client_secret", google_client_secret, + "redirect_uri", GOOGLE_REDIRECT_URI, + "grant_type", "authorization_code" + ); + + ResponseEntity responseEntity = restTemplate.postForEntity(GOOGLE_TOKEN_URL, params, String.class); + + return parseGoogleIdToken(responseEntity); + } + + @Override + public String getProvider() { + return String.valueOf(SocialType.GOOGLE).toLowerCase(); + } + + @Transactional + @Override + public UserInfo getUserInfo(String idToken) { + String decodePayload = getDecodePayload(idToken); + + try { + return objectMapper.readValue(decodePayload, UserInfo.class); + } catch (JsonProcessingException e) { + throw new OAuthException("id 토큰을 읽을 수 없습니다."); + } + } + + private JsonNode parseGoogleIdToken(ResponseEntity responseEntity) { + if (responseEntity.getStatusCode().is2xxSuccessful()) { + String responseBody = responseEntity.getBody(); + try { + JsonNode jsonNode = objectMapper.readTree(responseBody); + return jsonNode.get("id_token"); + } catch (Exception e) { + throw new RuntimeException("ID 토큰을 파싱하는데 실패했습니다.", e); + } + } + + throw new RuntimeException("구글 엑세스 토큰을 가져오는데 실패했습니다."); + } + + private String getDecodePayload(String idToken) { + String payload = getPayload(idToken); + + return new String(Base64.getUrlDecoder().decode(payload), StandardCharsets.UTF_8); + } + + private String getPayload(String idToken) { + return idToken.split(JWT_DELIMITER)[1]; + } + +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthService.java new file mode 100644 index 00000000..1a0a1950 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthService.java @@ -0,0 +1,107 @@ +package shop.kkeujeok.kkeujeokbackend.global.oauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; +import shop.kkeujeok.kkeujeokbackend.auth.application.AuthService; +import shop.kkeujeok.kkeujeokbackend.global.oauth.exception.OAuthException; +import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Service +@Transactional(readOnly = true) +@Slf4j +public class KakaoAuthService implements AuthService { + + private final String KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; + @Value("${oauth.kakao.rest-api-key}") + private String restApiKey; + @Value("${oauth.kakao.redirect-url}") + private String redirectUri; + + private static final String JWT_DELIMITER = "\\."; + + private final ObjectMapper objectMapper; + + public KakaoAuthService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + + public JsonNode getKakaoAccessToken(String code) { + RestTemplate rt = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", restApiKey); + params.add("redirect_uri", redirectUri); + params.add("code", code); + + HttpEntity> kakaoTokenRequest = new HttpEntity<>(params, headers); + + ResponseEntity response = rt.exchange( + KAKAO_TOKEN_URL, + HttpMethod.POST, + kakaoTokenRequest, + String.class + ); + + + if (response.getStatusCode().is2xxSuccessful()) { + String responseBody = response.getBody(); + try { + JsonNode jsonNode = objectMapper.readTree(responseBody); + return jsonNode.get("id_token"); + } catch (Exception e) { + throw new RuntimeException("ID 토큰을 파싱하는데 실패했습니다.", e); + } + } + throw new RuntimeException("구글 엑세스 토큰을 가져오는데 실패했습니다."); + + } + + @Override + public String getProvider() { + return String.valueOf(SocialType.KAKAO).toLowerCase(); + } + + @Transactional + @Override + public UserInfo getUserInfo(String idToken) { + String decodePayload = getDecodePayload(idToken); + + try { + return objectMapper.readValue(decodePayload, UserInfo.class); + } catch (JsonProcessingException e) { + throw new OAuthException("id 토큰을 읽을 수 없습니다."); + } + } + + private String getDecodePayload(String idToken) { + String payload = getPayload(idToken); + + return new String(Base64.getUrlDecoder().decode(payload), StandardCharsets.UTF_8); + } + + private String getPayload(String idToken) { + return idToken.split(JWT_DELIMITER)[1]; + } + +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/exception/OAuthException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/exception/OAuthException.java new file mode 100644 index 00000000..c54f5be5 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/oauth/exception/OAuthException.java @@ -0,0 +1,13 @@ +package shop.kkeujeok.kkeujeokbackend.global.oauth.exception; + +import shop.kkeujeok.kkeujeokbackend.global.error.exception.AuthGroupException; + +public class OAuthException extends AuthGroupException { + public OAuthException(String message) { + super(message); + } + + public OAuthException() { + this("OAuth 서버와의 통신 과정에서 문제가 발생했습니다."); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java index f55f487e..904172c2 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java @@ -5,16 +5,16 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; +import shop.kkeujeok.kkeujeokbackend.global.entity.Status; @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +@NoArgsConstructor +public class Member extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "member_id") - private Long memberId; + @Enumerated(EnumType.STRING) + private Status status; private boolean firstLogin; @@ -30,13 +30,11 @@ public class Member { @Enumerated(value = EnumType.STRING) private SocialType socialType; - // @Schema(description = "닉네임", example = "웅이") private String nickname; - - @Builder - private Member(Role role, String email, String name, String picture, SocialType socialType, boolean firstLogin, String nickname) { + private Member(Status status, Role role, String email, String name, String picture, SocialType socialType, boolean firstLogin, String nickname) { + this.status = status; this.role = role; this.email = email; this.name = name; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a84268cb..0609bd1c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,3 +34,11 @@ token: access : ${token.expire.time.access} refresh : ${token.expire.time.refresh} +GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI} +client_id: ${GOOGLE_CLIENT_ID} +client_secret: ${GOOGLE_CLIENT_SECRET} + +oauth: + kakao: + rest-api-key: ${KAKAO_REST_API_KEY} + redirect-url: ${KAKAO_REDIRECT_URL} \ No newline at end of file diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthControllerTest.java new file mode 100644 index 00000000..f727070c --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/api/AuthControllerTest.java @@ -0,0 +1,4 @@ +package shop.kkeujeok.kkeujeokbackend.auth.api; + +public class AuthControllerTest { +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberServiceTest.java new file mode 100644 index 00000000..e454378f --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthMemberServiceTest.java @@ -0,0 +1,125 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.MemberLoginResDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.UserInfo; +import shop.kkeujeok.kkeujeokbackend.global.entity.Status; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; +import shop.kkeujeok.kkeujeokbackend.member.domain.Role; +import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; +import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuthMemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private AuthMemberService authMemberService; + + private UserInfo userInfo; + private SocialType provider; + private Member member; + + @BeforeEach + void setUp() { + userInfo = new UserInfo("이메일", "이름", "사진", "닉네임"); + provider = SocialType.GOOGLE; + member = Member.builder() + .status(Status.A) + .email(userInfo.email()) + .name(userInfo.name()) + .picture(userInfo.picture()) + .socialType(provider) + .role(Role.ROLE_USER) + .firstLogin(true) + .nickname(userInfo.nickname()) + .build(); + } + + @DisplayName("신규 회원을 저장합니다.") + @Test + void 신규_회원을_저장합니다() { + when(memberRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(memberRepository.save(any(Member.class))).thenReturn(member); + + MemberLoginResDto result = authMemberService.saveUserInfo(userInfo, provider); + + assertThat(result).isNotNull(); + verify(memberRepository).findByEmail(userInfo.email()); + verify(memberRepository).save(any(Member.class)); + } + + @DisplayName("회원 정보가 올바르게 저장되는지 확인합니다.") + @Test + void 회원_정보가_올바르게_저장되는지_확인합니다() { + when(memberRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + MemberLoginResDto result = authMemberService.saveUserInfo(userInfo, provider); + + assertThat(result).isNotNull(); + assertThat(result.findMember().getEmail()).isEqualTo(userInfo.email()); + assertThat(result.findMember().getName()).isEqualTo(userInfo.name()); + verify(memberRepository).findByEmail(userInfo.email()); + verify(memberRepository).save(any(Member.class)); + } + + @DisplayName("소셜 타입이 일치하지 않는 경우 예외를 던집니다.") + @Test + void 소셜_타입이_일치하지_않는_경우_예외를_던집니다() { + when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(member)); + SocialType invalidProvider = SocialType.KAKAO; + + assertThrows(RuntimeException.class, () -> authMemberService.saveUserInfo(userInfo, invalidProvider)); + + verify(memberRepository).findByEmail(userInfo.email()); + } + + @DisplayName("이메일이 null인 경우 예외를 던집니다.") + @Test + void 이메일이_null인_경우_예외를_던집니다() { + UserInfo invalidUserInfo = new UserInfo(null, "이름", "사진", "닉네임"); + + assertThrows(RuntimeException.class, () -> authMemberService.saveUserInfo(invalidUserInfo, provider)); + } + + @DisplayName("이름이 null인 경우 예외를 던집니다.") + @Test + void 이름이_null인_경우_예외를_던집니다() { + UserInfo userInfoWithNullName = new UserInfo("이메일", null, "사진", "닉네임"); + + assertThrows(RuntimeException.class, () -> authMemberService.saveUserInfo(userInfoWithNullName, provider)); + } + + @DisplayName("사진이 null인 경우 예외를 던집니다.") + @Test + void 사진이_null인_경우_예외를_던집니다() { + UserInfo userInfoWithNullPicture = new UserInfo("이메일", "이름", null, "닉네임"); + + assertThrows(RuntimeException.class, () -> authMemberService.saveUserInfo(userInfoWithNullPicture, provider)); + } + + @DisplayName("닉네임이 null인 경우 예외를 던집니다.") + @Test + void 닉네임이_null인_경우_예외를_던집니다() { + UserInfo userInfoWithNullNickname = new UserInfo("이메일", "이름", "사진", null); + + assertThrows(RuntimeException.class, () -> authMemberService.saveUserInfo(userInfoWithNullNickname, provider)); + } +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactoryTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactoryTest.java new file mode 100644 index 00000000..bebffe70 --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/AuthServiceFactoryTest.java @@ -0,0 +1,45 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuthServiceFactoryTest { + + @Mock + private AuthService authService1; + + @Mock + private AuthService authService2; + + private AuthServiceFactory authServiceFactory; + + @BeforeEach + void setUp() { + when(authService1.getProvider()).thenReturn("provider1"); + when(authService2.getProvider()).thenReturn("provider2"); + + List authServiceList = Arrays.asList(authService1, authService2); + authServiceFactory = new AuthServiceFactory(authServiceList); + } + + @DisplayName("특정 provider에 맞는 AuthService를 반환합니다") + @Test + void 특정_provider에_맞는_AuthService를_반환합니다() { + AuthService result = authServiceFactory.getAuthService("provider1"); + assertThat(result).isEqualTo(authService1); + + result = authServiceFactory.getAuthService("provider2"); + assertThat(result).isEqualTo(authService2); + } +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenServiceTest.java new file mode 100644 index 00000000..85e1986d --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/auth/application/TokenServiceTest.java @@ -0,0 +1,105 @@ +package shop.kkeujeok.kkeujeokbackend.auth.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.RefreshTokenReqDto; +import shop.kkeujeok.kkeujeokbackend.auth.api.dto.response.MemberLoginResDto; +import shop.kkeujeok.kkeujeokbackend.global.jwt.TokenProvider; +import shop.kkeujeok.kkeujeokbackend.global.jwt.api.dto.TokenDto; +import shop.kkeujeok.kkeujeokbackend.global.jwt.domain.Token; +import shop.kkeujeok.kkeujeokbackend.global.jwt.domain.repository.TokenRepository; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; +import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TokenServiceTest { + + @Mock + private TokenProvider tokenProvider; + + @Mock + private TokenRepository tokenRepository; + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private TokenService tokenService; + + private MemberLoginResDto memberLoginResDto; + private TokenDto tokenDto; + private Member member; + private Token token; + + @BeforeEach + void setUp() { + member = Member.builder().email("test@example.com").build(); + + memberLoginResDto = mock(MemberLoginResDto.class); + tokenDto = mock(TokenDto.class); + token = mock(Token.class); + + when(memberLoginResDto.findMember()).thenReturn(member); + when(tokenProvider.generateToken(anyString())).thenReturn(tokenDto); + when(tokenRepository.findByMember(any(Member.class))).thenReturn(Optional.of(token)); + when(tokenDto.refreshToken()).thenReturn("new-refresh-token"); + } + + @DisplayName("accessToken과 refreshToken을 생성합니다.") + @Test + void accessToken과_refreshToken을_생성합니다() { + when(tokenRepository.existsByMember(any(Member.class))).thenReturn(false); + + TokenDto result = tokenService.getToken(memberLoginResDto); + + assertNotNull(result); + verify(tokenProvider).generateToken(member.getEmail()); + verify(tokenRepository).existsByMember(member); + verify(tokenRepository).save(any(Token.class)); + verify(token).refreshTokenUpdate("new-refresh-token"); + } + +// 하다가 벽느낀 테스트. +// @DisplayName("refreshToken으로 accessToken를 재생성한다.") +// @Test +// void refreshToken으로_accessToken를_재생성한다.() { +// // given +// String refreshToken = "refresh-token"; +// RefreshTokenReqDto refreshTokenReqDto = new RefreshTokenReqDto(refreshToken); +// Token token = new Token(member, refreshToken); +// +// when(tokenRepository.existsByRefreshToken(refreshToken)).thenReturn(true); +// when(tokenProvider.validateToken(refreshToken)).thenReturn(true); +// when(tokenRepository.findByRefreshToken(refreshToken)).thenReturn(Optional.of(token)); +// // Here we mock the memberRepository.findById to handle null or any Long +// when(memberRepository.findById(anyLong())).thenAnswer(invocation -> { +// Long id = invocation.getArgument(0); +// return id == null ? Optional.empty() : Optional.of(member); +// }); +// when(tokenProvider.generateAccessTokenByRefreshToken(anyString(), anyString())).thenReturn(tokenDto); +// +// // when +// TokenDto result = tokenService.generateAccessToken(refreshTokenReqDto); +// +// // then +// assertNotNull(result); +// verify(tokenRepository).existsByRefreshToken(refreshToken); +// verify(tokenProvider).validateToken(refreshToken); +// verify(tokenRepository).findByRefreshToken(refreshToken); +// verify(memberRepository).findById(anyLong()); +// verify(tokenProvider).generateAccessTokenByRefreshToken(member.getEmail(), refreshToken); +// } +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/TokenProviderTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/TokenProviderTest.java index 9830848c..763c26b1 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/TokenProviderTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/TokenProviderTest.java @@ -24,9 +24,9 @@ void init() { tokenProvider.init(); } - @DisplayName("엑세스 토큰을 생성한다.") + @DisplayName("엑세스 토큰을 생성합니다.") @Test - void 엑세스_토큰을_생성한다() { + void 엑세스_토큰을_생성합니다() { // given String email = "inho@gmail.com"; @@ -38,9 +38,9 @@ void init() { assertThat(actual.split("\\.")).hasSize(3); } - @DisplayName("리프레시 토큰을 생성한다.") + @DisplayName("리프레시 토큰을 생성합니다.") @Test - void 리프레시_토큰을_생성한다() { + void 리프레시_토큰을_생성합니다() { // given, when String actual = tokenProvider.generateRefreshToken(); @@ -50,9 +50,9 @@ void init() { assertThat(actual.split("\\.")).hasSize(3); } - @DisplayName("토큰들을 반환한다.") + @DisplayName("토큰들을 반환합니다.") @Test - void 토큰들을_반환한다() { + void 토큰들을_반환합니다() { // given String email = "inho@gmail.com"; @@ -63,9 +63,9 @@ void init() { assertThat(actual).isNotNull(); // 토큰이 null이 아닌지 확인 } - @DisplayName("리프레시 토큰으로 엑세스 토큰을 반환한다.") + @DisplayName("리프레시 토큰으로 엑세스 토큰을 반환합니다.") @Test - void 리프레시_토큰으로_엑세스_토큰을_반환한다() { + void 리프레시_토큰으로_엑세스_토큰을_반환합니다() { // given String refreshToken = "refreshToken"; String email = "inho@gmail.com"; @@ -77,9 +77,9 @@ void init() { assertThat(actual).isNotNull(); } - @DisplayName("토큰을 검증하여 유효하지 않으면 false를 반환한다.") + @DisplayName("토큰을 검증하여 유효하지 않으면 false를 반환합니다.") @Test - void 토큰을_검증하여_유효하지_않으면_false를_반환한다() { + void 토큰을_검증하여_유효하지_않으면_false를_반환합니다() { // given String malformedToken = "malformedToken"; diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/TokenTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/TokenTest.java new file mode 100644 index 00000000..9e32a868 --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/TokenTest.java @@ -0,0 +1,28 @@ +package shop.kkeujeok.kkeujeokbackend.global.jwt.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TokenTest { + @DisplayName("refresh token을 교체한다.") + @Test + void refresh_token을_교체한다() { + // given + Member 인호 = new Member(); + String refreshToken = "adasaegsfadasdasfgfgrgredksgdffa"; + Token oAuthToken = new Token(인호, refreshToken); + + String updatedRefreshToken = "dfgsbnskjglnafgkajfnakfjgngejlkrqgn"; + + // when + oAuthToken.refreshTokenUpdate(updatedRefreshToken); + + // then + assertThat(oAuthToken.getRefreshToken()).isEqualTo(updatedRefreshToken); + } + + +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/repository/TokenRepositoryTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/repository/TokenRepositoryTest.java new file mode 100644 index 00000000..d487ea94 --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/jwt/domain/repository/TokenRepositoryTest.java @@ -0,0 +1,91 @@ +package shop.kkeujeok.kkeujeokbackend.global.jwt.domain.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import shop.kkeujeok.kkeujeokbackend.global.jwt.domain.Token; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TokenRepositoryTest { + + @Mock + private TokenRepository tokenRepository; + + private Member member; + private Token token; + + @BeforeEach + void setUp() { + member = Member.builder().email("test@example.com").build(); + token = Token.builder().member(member).refreshToken("refresh-token").build(); + } + + @DisplayName("member로 token이 존재하는지 확인한다.") + + @Test + void member로_token이_존재하는지_확인한다() { + // given + when(tokenRepository.existsByMember(any(Member.class))).thenReturn(true); + + // when + boolean exists = tokenRepository.existsByMember(member); + + // then + assertThat(exists).isTrue(); + } + + @DisplayName("member로 token을 찾는다.") + + @Test + void member로_token을_찾는다() { + // given + when(tokenRepository.findByMember(any(Member.class))).thenReturn(Optional.of(token)); + + // when + Optional foundToken = tokenRepository.findByMember(member); + + // then + assertThat(foundToken).isPresent(); + assertThat(foundToken.get().getMember()).isEqualTo(member); + } + + @DisplayName("refreshToken으로 token이 존재하는지 확인한다.") + + @Test + void refreshToken으로_token이_존재하는지_확인한다() { + // given + when(tokenRepository.existsByRefreshToken(anyString())).thenReturn(true); + + // when + boolean exists = tokenRepository.existsByRefreshToken("refresh-token"); + + // then + assertThat(exists).isTrue(); + } + + @DisplayName("refreshToken으로 token을 찾는다.") + + @Test + void refreshToken으로_token을_찾는다() { + // given + when(tokenRepository.findByRefreshToken(anyString())).thenReturn(Optional.of(token)); + + // when + Optional foundToken = tokenRepository.findByRefreshToken("refresh-token"); + + // then + assertThat(foundToken).isPresent(); + assertThat(foundToken.get().getRefreshToken()).isEqualTo("refresh-token"); + } +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthServiceTest.java new file mode 100644 index 00000000..5dd80b20 --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/GoogleAuthServiceTest.java @@ -0,0 +1,4 @@ +package shop.kkeujeok.kkeujeokbackend.global.oauth; + +public class GoogleAuthServiceTest { +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthServiceTest.java new file mode 100644 index 00000000..dca0176b --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/global/oauth/KakaoAuthServiceTest.java @@ -0,0 +1,4 @@ +package shop.kkeujeok.kkeujeokbackend.global.oauth; + +public class KakaoAuthServiceTest { +} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/member/domain/repository/MemberRepositoryTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/domain/repository/MemberRepositoryTest.java new file mode 100644 index 00000000..65d46b62 --- /dev/null +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/domain/repository/MemberRepositoryTest.java @@ -0,0 +1,41 @@ +package shop.kkeujeok.kkeujeokbackend.member.domain.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MemberRepositoryTest { + + @Mock + private MemberRepository memberRepository; + + @DisplayName("email로 member를 찾습니다.") + @Test + void email로_member를_찾습니다() { + // given + String email = "test@example.com"; + Member member = Member.builder() + .email(email) + .build(); + + when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(member)); + + // when + Optional foundMember = memberRepository.findByEmail(email); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get().getEmail()).isEqualTo(email); + } +}