diff --git a/build.gradle b/build.gradle index 43cd199a..5f8fdfaf 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.testng:testng:7.1.0' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' @@ -43,6 +44,11 @@ dependencies { // Actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // QueryDSL implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta" annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta" diff --git a/src/main/java/org/noostak/auth/api/OauthController.java b/src/main/java/org/noostak/auth/api/OauthController.java new file mode 100644 index 00000000..4fdacea4 --- /dev/null +++ b/src/main/java/org/noostak/auth/api/OauthController.java @@ -0,0 +1,64 @@ +package org.noostak.auth.api; + + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.noostak.auth.application.OauthService; +import org.noostak.auth.application.OauthServiceFactory; +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.noostak.auth.common.success.AuthSuccessCode; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.SignUpResponse; +import org.noostak.global.success.SuccessResponse; +import org.noostak.auth.application.AuthInfoService; +import org.noostak.auth.dto.SignUpRequest; +import org.noostak.member.application.MemberService; +import org.noostak.member.domain.Member; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@Slf4j +@RequestMapping("/api/v1/auth") +public class OauthController { + + private final OauthServiceFactory oauthServiceFactory; + private final AuthInfoService authInfoService; + private final MemberService memberService; + + @PostMapping("/sign-up") + public ResponseEntity signUp(HttpServletRequest request, @ModelAttribute SignUpRequest requestDto){ + String code = request.getHeader("Authorization"); + + // authType 을 기준으로 OauthService 선택하기 + String authType = requestDto.getAuthType().toUpperCase(); + OauthService oauthService = oauthServiceFactory.getService(authType); + + // code를 통해서 AccessToken 및 RefreshToken 가져오기 + JwtToken jwtToken = oauthService.requestToken(code); + String accessToken = jwtToken.getAccessToken(); + log.info("jwtToken : {}", jwtToken); + + // 소셜 로그인 진행하기 + AuthId authId = oauthService.login(accessToken); + + + // 동일 소셜 계정으로 가입이 되어있는지 확인하기 + if(authInfoService.hasAuthInfo(authId)){ + throw new AuthException(AuthErrorCode.AUTHID_ALREADY_EXISTS,authId.value()); + } + + // 멤버 생성하기 + Member member = memberService.createMember(requestDto); + + // 멤버와 연관된 AuthInfo 생성하기 + SignUpResponse response = + authInfoService.createAuthInfo(authType, authId, jwtToken, member); + + return ResponseEntity.ok((SuccessResponse.of(AuthSuccessCode.SIGNUP_COMPLETED,response))); + } +} diff --git a/src/main/java/org/noostak/auth/application/AuthInfoService.java b/src/main/java/org/noostak/auth/application/AuthInfoService.java new file mode 100644 index 00000000..21163ca4 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/AuthInfoService.java @@ -0,0 +1,14 @@ +package org.noostak.auth.application; + +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.SignUpResponse; +import org.noostak.member.domain.Member; + +public interface AuthInfoService { + SignUpResponse createAuthInfo(String authType, AuthId authId, JwtToken jwtToken, Member member); + + boolean hasAuthInfo(String authId); + + boolean hasAuthInfo(AuthId authId); +} diff --git a/src/main/java/org/noostak/auth/application/AuthInfoServiceImpl.java b/src/main/java/org/noostak/auth/application/AuthInfoServiceImpl.java new file mode 100644 index 00000000..146ec997 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/AuthInfoServiceImpl.java @@ -0,0 +1,63 @@ +package org.noostak.auth.application; + +import lombok.RequiredArgsConstructor; +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.domain.AuthInfo; +import org.noostak.auth.domain.AuthInfoRepository; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.domain.vo.AuthType; +import org.noostak.auth.domain.vo.RefreshToken; +import org.noostak.auth.dto.SignUpResponse; +import org.noostak.member.domain.Member; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +@RequiredArgsConstructor +public class AuthInfoServiceImpl implements AuthInfoService{ + + private final AuthInfoRepository authInfoRepository; + @Override + @Transactional + public SignUpResponse createAuthInfo(String authType, AuthId authId, JwtToken jwtToken, Member member) { + AuthInfo newAuthInfo = createAuthInfo( + AuthType.from(authType), + authId, + RefreshToken.from(jwtToken.getRefreshToken()), + member + ); + + saveAuthInfo(newAuthInfo); + + return SignUpResponse.of( + jwtToken.getAccessToken(), + jwtToken.getRefreshToken(), + member.getId(), + authType + ); + } + + private AuthInfo createAuthInfo(AuthType authType, AuthId authId, RefreshToken refreshToken, Member member) { + return AuthInfo.of( + authType, + authId, + refreshToken, + member + ); + } + + private AuthInfo saveAuthInfo(AuthInfo authInfo){ + return authInfoRepository.save(authInfo); + } + + @Override + public boolean hasAuthInfo(String authId){ + return authInfoRepository.hasAuthInfoByAuthId(AuthId.from(authId)); + } + + @Override + public boolean hasAuthInfo(AuthId authId) { + return authInfoRepository.hasAuthInfoByAuthId(authId); + } +} diff --git a/src/main/java/org/noostak/auth/application/GoogleApi.java b/src/main/java/org/noostak/auth/application/GoogleApi.java new file mode 100644 index 00000000..1845381d --- /dev/null +++ b/src/main/java/org/noostak/auth/application/GoogleApi.java @@ -0,0 +1,15 @@ +package org.noostak.auth.application; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GoogleApi { + TOKEN_REQUEST("https://oauth2.googleapis.com/token"), + USER_INFO("https://www.googleapis.com/oauth2/v2/userinfo") + ; + + private final String url; +} diff --git a/src/main/java/org/noostak/auth/application/GoogleService.java b/src/main/java/org/noostak/auth/application/GoogleService.java new file mode 100644 index 00000000..97fabc3c --- /dev/null +++ b/src/main/java/org/noostak/auth/application/GoogleService.java @@ -0,0 +1,5 @@ +package org.noostak.auth.application; + +public interface GoogleService extends OauthService { + +} diff --git a/src/main/java/org/noostak/auth/application/GoogleServiceImpl.java b/src/main/java/org/noostak/auth/application/GoogleServiceImpl.java new file mode 100644 index 00000000..b44a0ef7 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/GoogleServiceImpl.java @@ -0,0 +1,77 @@ +package org.noostak.auth.application; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.application.jwt.JwtTokenProvider; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.*; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GoogleServiceImpl implements GoogleService{ + + private final GoogleTokenRequestFactory googleTokenRequestFactory; + private final RestClient restClient; + + @Override + public void requestAccessToken(String refreshToken) { + + } + + + @Override + public TokenInfo fetchTokenInfo(String accessToken) { + return null; + } + + + @Override + public JwtToken requestToken(String code) { + String url = GoogleApi.TOKEN_REQUEST.getUrl(); + + GoogleTokenRequest request = googleTokenRequestFactory.createRequest(code); + + GoogleTokenResponse response = + restClient.postRequest(url, + request.getUrlEncodedParams(), + GoogleTokenResponse.class); + + log.info("googleTokenResponse: {}",response); + response.validate(); + + return JwtTokenProvider.createToken(response.getAccessToken(),response.getRefreshToken()); + } + + @Override + public AuthId login(String accessToken) { + String url = GoogleApi.USER_INFO.getUrl(); + + HttpHeaders headers = makeAuthorizationBearerTokenHeader(accessToken); + + GoogleUserInfoResponse response = + restClient.getRequest(url, headers, GoogleUserInfoResponse.class); + + response.validate(); + + return AuthId.from(response.getId()); + } + + public HttpHeaders makeAuthorizationBearerTokenHeader(String token){ + HttpHeaders headers = new HttpHeaders(); + + if(token == null || token.isEmpty() || token.isBlank()){ + throw new AuthException(AuthErrorCode.INVALID_TOKEN); + } + + headers.set("Authorization", "Bearer " + token); + + return headers; + } +} diff --git a/src/main/java/org/noostak/auth/application/GoogleTokenRequestFactory.java b/src/main/java/org/noostak/auth/application/GoogleTokenRequestFactory.java new file mode 100644 index 00000000..01d89d9a --- /dev/null +++ b/src/main/java/org/noostak/auth/application/GoogleTokenRequestFactory.java @@ -0,0 +1,29 @@ +package org.noostak.auth.application; + +import org.noostak.auth.dto.GoogleTokenRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + + +@Component +public class GoogleTokenRequestFactory { + + @Value("${oauth-property.google.client_id}") + private final String clientId; + + @Value("${oauth-property.google.redirect_uri}") + private final String redirectUri; + + @Value("${oauth-property.google.client_secret}") + private final String clientSecret; + + public GoogleTokenRequestFactory(String clientId, String redirectUri, String clientSecret) { + this.clientId = clientId; + this.redirectUri = redirectUri; + this.clientSecret = clientSecret; + } + + public GoogleTokenRequest createRequest(String code) { + return GoogleTokenRequest.of(clientId, redirectUri, code, clientSecret); + } +} \ No newline at end of file diff --git a/src/main/java/org/noostak/auth/application/KaKaoApi.java b/src/main/java/org/noostak/auth/application/KaKaoApi.java new file mode 100644 index 00000000..3d346e93 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/KaKaoApi.java @@ -0,0 +1,16 @@ +package org.noostak.auth.application; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum KaKaoApi { + TOKEN_REQUEST("https://kauth.kakao.com/oauth/token"), + FETCH_TOKEN("https://kapi.kakao.com/v1/user/access_token_info"), + USER_INFO("https://kapi.kakao.com/v2/user/me") + ; + + private final String url; +} diff --git a/src/main/java/org/noostak/auth/application/KakaoService.java b/src/main/java/org/noostak/auth/application/KakaoService.java new file mode 100644 index 00000000..adb21db5 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/KakaoService.java @@ -0,0 +1,5 @@ +package org.noostak.auth.application; + +public interface KakaoService extends OauthService { + +} diff --git a/src/main/java/org/noostak/auth/application/KakaoServiceImpl.java b/src/main/java/org/noostak/auth/application/KakaoServiceImpl.java new file mode 100644 index 00000000..4ac653cd --- /dev/null +++ b/src/main/java/org/noostak/auth/application/KakaoServiceImpl.java @@ -0,0 +1,80 @@ +package org.noostak.auth.application; + + +import lombok.RequiredArgsConstructor; +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.application.jwt.JwtTokenProvider; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.*; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class KakaoServiceImpl implements KakaoService{ + + private final KakaoTokenRequestFactory kakaoTokenRequestFactory; + private final RestClient restClient; + @Override + public void requestAccessToken(String refreshToken) { + + } + + @Override + public TokenInfo fetchTokenInfo(String accessToken) { + String url = KaKaoApi.FETCH_TOKEN.getUrl(); + HttpHeaders headers = makeAuthorizationBearerTokenHeader(accessToken); + + KakaoTokenInfoResponse response = + restClient.postRequest(url, headers, KakaoTokenInfoResponse.class); + + response.validate(); + + return TokenInfo.of(response.getId()); + } + + + @Override + public JwtToken requestToken(String code) { + String url = KaKaoApi.TOKEN_REQUEST.getUrl(); + + KakaoTokenRequest request = kakaoTokenRequestFactory.createRequest(code); + + KakaoTokenResponse response = + restClient.postRequest(url, + request.getUrlEncodedParams(), + KakaoTokenResponse.class); + + response.validate(); + + return JwtTokenProvider.createToken(response.getAccessToken(),response.getRefreshToken()); + } + + @Override + public AuthId login(String accessToken) { + String url = KaKaoApi.USER_INFO.getUrl(); + + HttpHeaders headers = makeAuthorizationBearerTokenHeader(accessToken); + + KakaoUserInfoResponse response = + restClient.postRequest(url, headers, KakaoUserInfoResponse.class); + + response.validate(); + + return AuthId.from(response.getId()); + } + + public HttpHeaders makeAuthorizationBearerTokenHeader(String token){ + HttpHeaders headers = new HttpHeaders(); + + if(token == null || token.isEmpty() || token.isBlank()){ + throw new AuthException(AuthErrorCode.INVALID_TOKEN); + } + + headers.set("Authorization", "Bearer " + token); + + return headers; + } +} diff --git a/src/main/java/org/noostak/auth/application/KakaoTokenRequestFactory.java b/src/main/java/org/noostak/auth/application/KakaoTokenRequestFactory.java new file mode 100644 index 00000000..99c53f21 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/KakaoTokenRequestFactory.java @@ -0,0 +1,25 @@ +package org.noostak.auth.application; + +import org.noostak.auth.dto.KakaoTokenRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + + +@Component +public class KakaoTokenRequestFactory { + + @Value("${oauth-property.kakao.client_id}") + private final String clientId; + + @Value("${oauth-property.kakao.redirect_uri}") + private final String redirectUri; + + public KakaoTokenRequestFactory(String clientId, String redirectUri) { + this.clientId = clientId; + this.redirectUri = redirectUri; + } + + public KakaoTokenRequest createRequest(String code) { + return KakaoTokenRequest.of(clientId, redirectUri, code); + } +} \ No newline at end of file diff --git a/src/main/java/org/noostak/auth/application/OauthService.java b/src/main/java/org/noostak/auth/application/OauthService.java new file mode 100644 index 00000000..c28df27e --- /dev/null +++ b/src/main/java/org/noostak/auth/application/OauthService.java @@ -0,0 +1,16 @@ +package org.noostak.auth.application; + +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.TokenInfo; + +public interface OauthService { + + void requestAccessToken(String refreshToken); // 액세스 토큰 재발급 + + TokenInfo fetchTokenInfo(String accessToken); // 토큰 정보 가져오기 + + JwtToken requestToken(String code); // 토큰 정보 발급 + + AuthId login(String accessToken); // 로그인 처리 +} diff --git a/src/main/java/org/noostak/auth/application/OauthServiceFactory.java b/src/main/java/org/noostak/auth/application/OauthServiceFactory.java new file mode 100644 index 00000000..19d705aa --- /dev/null +++ b/src/main/java/org/noostak/auth/application/OauthServiceFactory.java @@ -0,0 +1,33 @@ +package org.noostak.auth.application; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.noostak.auth.domain.vo.AuthType; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class OauthServiceFactory { + private final Map serviceMap = new HashMap<>(); + private final KakaoService kakaoService; + private final GoogleService googleService; + + @PostConstruct + public void init() { + serviceMap.put(AuthType.KAKAO.getName(), kakaoService); + serviceMap.put(AuthType.GOOGLE.getName(), googleService); + } + + public OauthService getService(String authType) { + if(!serviceMap.containsKey(authType)){ + throw new AuthException(AuthErrorCode.AUTH_TYPE_NOT_EXISTS, authType); + } + + return serviceMap.get(authType); + } +} diff --git a/src/main/java/org/noostak/auth/application/RestClient.java b/src/main/java/org/noostak/auth/application/RestClient.java new file mode 100644 index 00000000..eb1c701c --- /dev/null +++ b/src/main/java/org/noostak/auth/application/RestClient.java @@ -0,0 +1,102 @@ +package org.noostak.auth.application; + +import lombok.extern.slf4j.Slf4j; +import org.noostak.auth.common.exception.RestClientErrorCode; +import org.noostak.auth.common.exception.RestClientException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@Slf4j +public class RestClient { + + private final RestTemplate restTemplate; + + public RestClient() { + this.restTemplate = new RestTemplate(); + } + + /** + * 기본적인 JSON 요청 + */ + public T postRequest(String url, R request, Class responseType) { + try { + T response = restTemplate.postForObject(url, request, responseType); + return validate(response); + } catch (Exception e) { + throw new RestClientException(RestClientErrorCode.REST_CLIENT_ERROR, e.getMessage()); + } + } + + /** + * x-www-form-urlencoded 요청 + */ + public T postRequest(String url, String urlParams, Class responseType) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity entity = new HttpEntity<>(urlParams, headers); + T response = restTemplate.postForObject(url, entity, responseType); + + return validate(response); + } catch (Exception e) { + throw new RestClientException(RestClientErrorCode.REST_CLIENT_ERROR, e.getMessage()); + } + } + + /** + * 커스텀 헤더와 함께 JSON 요청 + */ + public T postRequest(String url, HttpHeaders headers, R request, Class responseType) { + try { + HttpEntity requestHttpEntity = new HttpEntity<>(request, headers); + T response = restTemplate.postForObject(url, requestHttpEntity, responseType); + return validate(response); + } catch (Exception e) { + throw new RestClientException(RestClientErrorCode.REST_CLIENT_ERROR, e.getMessage()); + } + } + + /** + * 커스텀 헤더만 포함된 요청 + */ + public T postRequest(String url, HttpHeaders headers, Class responseType) { + try { + HttpEntity entity = new HttpEntity<>(headers); + T response = restTemplate.postForObject(url, entity, responseType); + + return validate(response); + } catch (Exception e) { + throw new RestClientException(RestClientErrorCode.REST_CLIENT_ERROR, e.getMessage()); + } + } + + public T getRequest(String url, HttpHeaders headers, Class responseType) { + try { + HttpEntity entity = new HttpEntity<>(headers); + T response = restTemplate.exchange(url, HttpMethod.GET,entity, responseType).getBody(); + return validate(response); + } catch (Exception e) { + throw new RestClientException(RestClientErrorCode.REST_CLIENT_ERROR, e.getMessage()); + } + } + + private T validate(T response){ + validateIsNull(response); + + return response; + } + + private void validateIsNull(T response){ + if (response == null) { + throw new RestClientException(RestClientErrorCode.RESPONSE_IS_NULL); + } + } +} + + diff --git a/src/main/java/org/noostak/auth/application/jwt/JwtToken.java b/src/main/java/org/noostak/auth/application/jwt/JwtToken.java new file mode 100644 index 00000000..d85f2c7b --- /dev/null +++ b/src/main/java/org/noostak/auth/application/jwt/JwtToken.java @@ -0,0 +1,15 @@ +package org.noostak.auth.application.jwt; + +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class JwtToken { + private String tokenType = "Bearer"; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/org/noostak/auth/application/jwt/JwtTokenProvider.java b/src/main/java/org/noostak/auth/application/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..d7d3700d --- /dev/null +++ b/src/main/java/org/noostak/auth/application/jwt/JwtTokenProvider.java @@ -0,0 +1,48 @@ +package org.noostak.auth.application.jwt; + + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; + +@Component +@Slf4j +public class JwtTokenProvider { + private final SecretKey secretKey; + + public JwtTokenProvider(String secretKey) { + log.info("secret: " + secretKey); + this.secretKey = Jwts.SIG.HS256.key().build(); + } + + public static JwtToken createToken(String accessToken, String refreshToken){ + return new JwtToken("Bearer",accessToken,refreshToken); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return true; + } catch (Exception e) { + return false; + } + } + + + public String getAuthId(String token) { + + // TODO: 카카오, 구글 별로 액세스 토큰에서 AuthId 추출하기 + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + } +} \ No newline at end of file diff --git a/src/main/java/org/noostak/auth/application/jwt/config/JwtTokenConfig.java b/src/main/java/org/noostak/auth/application/jwt/config/JwtTokenConfig.java new file mode 100644 index 00000000..64456986 --- /dev/null +++ b/src/main/java/org/noostak/auth/application/jwt/config/JwtTokenConfig.java @@ -0,0 +1,18 @@ +package org.noostak.auth.application.jwt.config; + +import org.noostak.auth.application.jwt.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JwtTokenConfig { + + @Value("${jwt.secret}") + private String secretKey; + + @Bean + public JwtTokenProvider jwtTokenProvider(String secretKey) { + return new JwtTokenProvider(secretKey); + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/AuthErrorCode.java b/src/main/java/org/noostak/auth/common/exception/AuthErrorCode.java new file mode 100644 index 00000000..e59003ec --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/AuthErrorCode.java @@ -0,0 +1,35 @@ +package org.noostak.auth.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.global.error.core.ErrorCode; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthErrorCode implements ErrorCode { + AUTH_TYPE_NOT_EXISTS(HttpStatus.NOT_FOUND, "입력된 AuthType을 찾을 수 없습니다. 입력값 : %s"), + AUTH_INFO_NOT_EXISTS(HttpStatus.NOT_FOUND, "입력된 AuthId를 찾을 수 없습니다. 입력값 : %s"), + AUTHID_ALREADY_EXISTS(HttpStatus.NOT_FOUND, "동일한 AuthId가 존재합니다. 입력값 : %s"), + API_ERROR_RESPONSE(HttpStatus.BAD_REQUEST, "외부 API 호출 도중 에러가 발생하였습니다. 에러내용: %s"), + UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."), + EMPTY_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 값이 비어있습니다."), + + ; + + public static final String PREFIX = "[AUTH ERROR] "; + + private final HttpStatus status; + private final String rawMessage; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return PREFIX + rawMessage; + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/AuthException.java b/src/main/java/org/noostak/auth/common/exception/AuthException.java new file mode 100644 index 00000000..6c6996ed --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/AuthException.java @@ -0,0 +1,15 @@ +package org.noostak.auth.common.exception; + + +import org.noostak.global.error.core.BaseException; + +public class AuthException extends BaseException { + + public AuthException(AuthErrorCode errorCode) { + super(errorCode); + } + + public AuthException(AuthErrorCode errorCode, Object ... args) { + super(errorCode,args); + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/GoogleApiErrorCode.java b/src/main/java/org/noostak/auth/common/exception/GoogleApiErrorCode.java new file mode 100644 index 00000000..16fae04e --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/GoogleApiErrorCode.java @@ -0,0 +1,29 @@ +package org.noostak.auth.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.global.error.core.ErrorCode; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GoogleApiErrorCode implements ErrorCode { + GOOGLE_API_ERROR(HttpStatus.NOT_FOUND, "구글 API 호출에 대해 에러 응답이 반환 되었습니다. 에러 정보 : %s"), + + ; + + public static final String PREFIX = "[GOOGLE API ERROR] "; + + private final HttpStatus status; + private final String rawMessage; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return PREFIX + rawMessage; + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/GoogleApiException.java b/src/main/java/org/noostak/auth/common/exception/GoogleApiException.java new file mode 100644 index 00000000..d5d87458 --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/GoogleApiException.java @@ -0,0 +1,13 @@ +package org.noostak.auth.common.exception; + + +public class GoogleApiException extends RuntimeException { + + public GoogleApiException(GoogleApiErrorCode errorCode) { + super(errorCode.getMessage()); + } + + public GoogleApiException(GoogleApiErrorCode errorCode, Object ... args) { + super(errorCode.getMessage(args)); + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/KakaoApiErrorCode.java b/src/main/java/org/noostak/auth/common/exception/KakaoApiErrorCode.java new file mode 100644 index 00000000..172123c2 --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/KakaoApiErrorCode.java @@ -0,0 +1,29 @@ +package org.noostak.auth.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.global.error.core.ErrorCode; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum KakaoApiErrorCode implements ErrorCode { + KAKAO_API_ERROR(HttpStatus.NOT_FOUND, "카카오 API 호출에 대해 에러 응답이 반환 되었습니다. 에러 정보 : %s"), + + ; + + public static final String PREFIX = "[KAKAO API ERROR] "; + + private final HttpStatus status; + private final String rawMessage; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return PREFIX + rawMessage; + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/KakaoApiException.java b/src/main/java/org/noostak/auth/common/exception/KakaoApiException.java new file mode 100644 index 00000000..182bd319 --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/KakaoApiException.java @@ -0,0 +1,13 @@ +package org.noostak.auth.common.exception; + + +public class KakaoApiException extends RuntimeException { + + public KakaoApiException(KakaoApiErrorCode errorCode) { + super(errorCode.getMessage()); + } + + public KakaoApiException(KakaoApiErrorCode errorCode, Object ... args) { + super(errorCode.getMessage(args)); + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/RestClientErrorCode.java b/src/main/java/org/noostak/auth/common/exception/RestClientErrorCode.java new file mode 100644 index 00000000..e6a66692 --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/RestClientErrorCode.java @@ -0,0 +1,30 @@ +package org.noostak.auth.common.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.global.error.core.ErrorCode; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum RestClientErrorCode implements ErrorCode { + REST_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "RestClient 에서 예기치않은 에러가 발생하였습니다. 에러내용: %s"), + RESPONSE_IS_NULL(HttpStatus.BAD_REQUEST, "RestClient의 응답이 비어있습니다."), + + ; + + public static final String PREFIX = "[REST CLIENT ERROR] "; + + private final HttpStatus status; + private final String rawMessage; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return PREFIX + rawMessage; + } +} diff --git a/src/main/java/org/noostak/auth/common/exception/RestClientException.java b/src/main/java/org/noostak/auth/common/exception/RestClientException.java new file mode 100644 index 00000000..b1a622a4 --- /dev/null +++ b/src/main/java/org/noostak/auth/common/exception/RestClientException.java @@ -0,0 +1,13 @@ +package org.noostak.auth.common.exception; + +import org.noostak.global.error.core.BaseException; + +public class RestClientException extends BaseException { + public RestClientException(RestClientErrorCode errorCode) { + super(errorCode); + } + + public RestClientException(RestClientErrorCode errorCode, Object ... args) { + super(errorCode,args); + } +} \ No newline at end of file diff --git a/src/main/java/org/noostak/auth/common/success/AuthSuccessCode.java b/src/main/java/org/noostak/auth/common/success/AuthSuccessCode.java new file mode 100644 index 00000000..6f63fadd --- /dev/null +++ b/src/main/java/org/noostak/auth/common/success/AuthSuccessCode.java @@ -0,0 +1,16 @@ +package org.noostak.auth.common.success; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.global.success.SuccessCode; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthSuccessCode implements SuccessCode { + SIGNUP_COMPLETED(HttpStatus.CREATED, "회원가입이 성공적으로 완료 되었습니다."), + ; + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/org/noostak/auth/domain/AuthInfo.java b/src/main/java/org/noostak/auth/domain/AuthInfo.java new file mode 100644 index 00000000..ebdf9e60 --- /dev/null +++ b/src/main/java/org/noostak/auth/domain/AuthInfo.java @@ -0,0 +1,47 @@ +package org.noostak.auth.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.domain.vo.AuthType; +import org.noostak.auth.domain.vo.RefreshToken; +import org.noostak.member.domain.Member; + + +@Entity +@Getter +@RequiredArgsConstructor +public class AuthInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long authInfoId; + + @Enumerated(EnumType.STRING) + @Column(name = "auth_type") + private AuthType authType; + + @Embedded + @AttributeOverride(name = "id", column = @Column(name = "auth_id")) + private AuthId authId; + + @Embedded + @AttributeOverride(name = "token", column = @Column(name = "refresh_token")) + private RefreshToken refreshToken; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + private AuthInfo(AuthType authType, AuthId authId, RefreshToken refreshToken, Member member) { + this.authType = authType; + this.authId = authId; + this.refreshToken = refreshToken; + this.member = member; + } + + public static AuthInfo of(AuthType type, AuthId authId, RefreshToken token, Member member) { + return new AuthInfo(type,authId,token,member); + } +} diff --git a/src/main/java/org/noostak/auth/domain/AuthInfoRepository.java b/src/main/java/org/noostak/auth/domain/AuthInfoRepository.java new file mode 100644 index 00000000..05edff00 --- /dev/null +++ b/src/main/java/org/noostak/auth/domain/AuthInfoRepository.java @@ -0,0 +1,25 @@ +package org.noostak.auth.domain; + +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.noostak.auth.domain.vo.AuthId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AuthInfoRepository extends JpaRepository { + + Optional findByAuthId(AuthId authid); + boolean existsAuthInfoByAuthId(AuthId code); + + default boolean hasAuthInfoByAuthId(AuthId authid){ + return this.existsAuthInfoByAuthId(authid); + } + + default AuthInfo getAuthInfoByAuthId(AuthId authid){ + return this.findByAuthId(authid) + .orElseThrow(()->new AuthException(AuthErrorCode.AUTH_INFO_NOT_EXISTS, authid)); + } +} diff --git a/src/main/java/org/noostak/member/domain/vo/AuthId.java b/src/main/java/org/noostak/auth/domain/vo/AuthId.java similarity index 96% rename from src/main/java/org/noostak/member/domain/vo/AuthId.java rename to src/main/java/org/noostak/auth/domain/vo/AuthId.java index 3bf45ef5..1ff7667d 100644 --- a/src/main/java/org/noostak/member/domain/vo/AuthId.java +++ b/src/main/java/org/noostak/auth/domain/vo/AuthId.java @@ -1,4 +1,4 @@ -package org.noostak.member.domain.vo; +package org.noostak.auth.domain.vo; import jakarta.persistence.Embeddable; import lombok.EqualsAndHashCode; diff --git a/src/main/java/org/noostak/auth/domain/vo/AuthType.java b/src/main/java/org/noostak/auth/domain/vo/AuthType.java new file mode 100644 index 00000000..0ba3ee2b --- /dev/null +++ b/src/main/java/org/noostak/auth/domain/vo/AuthType.java @@ -0,0 +1,24 @@ +package org.noostak.auth.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum AuthType { + KAKAO("KAKAO"), + GOOGLE("GOOGLE"), + APPLE("APPLE"); + + private final String name; + + public static AuthType from(String givenAuthType){ + return Arrays.stream(AuthType.values()) + .filter(type->type.name().equals(givenAuthType)).findFirst() + .orElseThrow(()->new AuthException(AuthErrorCode.AUTH_TYPE_NOT_EXISTS,givenAuthType)); + } +} diff --git a/src/main/java/org/noostak/auth/domain/vo/Code.java b/src/main/java/org/noostak/auth/domain/vo/Code.java new file mode 100644 index 00000000..61642f58 --- /dev/null +++ b/src/main/java/org/noostak/auth/domain/vo/Code.java @@ -0,0 +1,47 @@ +package org.noostak.auth.domain.vo; + +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import org.noostak.member.common.MemberErrorCode; +import org.noostak.member.common.MemberException; + +@Embeddable +@EqualsAndHashCode +public class Code { + + private final String code; + + protected Code() { + this.code = null; + } + + private Code(String code) { + validate(code); + this.code = code; + } + + public static Code from(String code) { + return new Code(code); + } + + public String value() { + return code; + } + + private void validate(String id) { + validateNotNull(id); + validateEmpty(id); + } + + private void validateEmpty(String code) { + if (code.isBlank()) { + throw new MemberException(MemberErrorCode.AUTH_ID_NOT_EMPTY); + } + } + + private void validateNotNull(String code) { + if (code == null) { + throw new MemberException(MemberErrorCode.AUTH_ID_NOT_NULL); + } + } +} diff --git a/src/main/java/org/noostak/auth/domain/vo/RefreshToken.java b/src/main/java/org/noostak/auth/domain/vo/RefreshToken.java new file mode 100644 index 00000000..60d237dc --- /dev/null +++ b/src/main/java/org/noostak/auth/domain/vo/RefreshToken.java @@ -0,0 +1,27 @@ +package org.noostak.auth.domain.vo; + + +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; + +@Embeddable +@EqualsAndHashCode +public class RefreshToken { + + private final String token; + + protected RefreshToken() { + this.token = null; + } + + private RefreshToken(String token) { + this.token = token; + } + + public static RefreshToken from(String token){ + return new RefreshToken(token); + } + public boolean expired(){ + return true; + } +} diff --git a/src/main/java/org/noostak/auth/dto/GoogleTokenRequest.java b/src/main/java/org/noostak/auth/dto/GoogleTokenRequest.java new file mode 100644 index 00000000..4e8237c5 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/GoogleTokenRequest.java @@ -0,0 +1,43 @@ +package org.noostak.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + + +@Getter +public class GoogleTokenRequest { + private String code; + private String clientId; + private String clientSecret; + private String redirectUri; + private String grantType; + + private GoogleTokenRequest(String clientId, String redirectUri, String code, String clientSecret) { + this.grantType = "authorization_code"; + this.clientId = clientId; + this.redirectUri = redirectUri; + this.code = code; + this.clientSecret = clientSecret; + } + + public static GoogleTokenRequest of(String clientId, String redirectUri, String code, String clientSecret){ + return new GoogleTokenRequest(clientId,redirectUri,code,clientSecret); + } + + public String getUrlEncodedParams() { + StringBuilder params = new StringBuilder(); + + params.append("grant_type=").append(URLEncoder.encode(grantType, StandardCharsets.UTF_8)); + params.append("&clientId=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8)); + params.append("&clientSecret=").append(URLEncoder.encode(clientSecret, StandardCharsets.UTF_8)); + params.append("&redirectUri=").append(URLEncoder.encode(redirectUri, StandardCharsets.UTF_8)); + params.append("&code=").append(URLEncoder.encode(code, StandardCharsets.UTF_8)); + params.append("&prompt=").append(URLEncoder.encode("consent", StandardCharsets.UTF_8)); + params.append("&access_type=").append(URLEncoder.encode("offline", StandardCharsets.UTF_8)); + + return params.toString(); + } +} diff --git a/src/main/java/org/noostak/auth/dto/GoogleTokenResponse.java b/src/main/java/org/noostak/auth/dto/GoogleTokenResponse.java new file mode 100644 index 00000000..71a3e3b6 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/GoogleTokenResponse.java @@ -0,0 +1,47 @@ +package org.noostak.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.noostak.auth.common.exception.GoogleApiErrorCode; +import org.noostak.auth.common.exception.GoogleApiException; + +import java.util.List; + +@Getter +@AllArgsConstructor +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class GoogleTokenResponse { + + private String accessToken; + private int expiresIn; + private String refreshToken; + private String tokenType; + private String idToken; + + + private int code; + private String message; + private List details; + + @Getter + @Setter + public static class ErrorDetail { + @JsonProperty("@type") + private String type; + private String reason; + private String domain; + private String metadata; + } + + public void validate() { + if (details != null && !details.isEmpty()) { + throw new GoogleApiException(GoogleApiErrorCode.GOOGLE_API_ERROR,details); + } + } +} diff --git a/src/main/java/org/noostak/auth/dto/GoogleUserInfoResponse.java b/src/main/java/org/noostak/auth/dto/GoogleUserInfoResponse.java new file mode 100644 index 00000000..fcb0ff9f --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/GoogleUserInfoResponse.java @@ -0,0 +1,48 @@ +package org.noostak.auth.dto; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.auth.common.exception.GoogleApiErrorCode; +import org.noostak.auth.common.exception.GoogleApiException; + + +@Getter +@AllArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class GoogleUserInfoResponse { + + private String id; + private String email; + private boolean verifiedEmail; + private String name; + private String givenName; + private String familyName; + private String picture; + private String locale; + + // 에러 응답 처리를 위한 필드 (선택적) + @JsonProperty("error") + private Error error; + + + @AllArgsConstructor + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Error { + private int code; + private String message; + private String status; + } + + public void validate() { + if (error != null) { + throw new GoogleApiException(GoogleApiErrorCode.GOOGLE_API_ERROR, error.getMessage()); + } + } +} + diff --git a/src/main/java/org/noostak/auth/dto/KakaoTokenInfoResponse.java b/src/main/java/org/noostak/auth/dto/KakaoTokenInfoResponse.java new file mode 100644 index 00000000..062bc410 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/KakaoTokenInfoResponse.java @@ -0,0 +1,27 @@ +package org.noostak.auth.dto; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.auth.common.exception.KakaoApiErrorCode; +import org.noostak.auth.common.exception.KakaoApiException; + +@Getter +@AllArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoTokenInfoResponse { + private String id; + private String expires_in; + private String app_id; + private String error; + private String errorDescription; + private String errorCode; + + public void validate(){ + if(error!=null){ + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_ERROR, errorDescription); + } + } +} diff --git a/src/main/java/org/noostak/auth/dto/KakaoTokenRequest.java b/src/main/java/org/noostak/auth/dto/KakaoTokenRequest.java new file mode 100644 index 00000000..f575e6db --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/KakaoTokenRequest.java @@ -0,0 +1,46 @@ +package org.noostak.auth.dto; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.ToString; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoTokenRequest { + + private String grantType; + + private String clientId; + + private String redirectUri; + + private String code; + + private KakaoTokenRequest(String clientId, String redirectUri, String code) { + this.grantType = "authorization_code"; + this.clientId = clientId; + this.redirectUri = redirectUri; + this.code = code; + } + + public static KakaoTokenRequest of(String clientId, String redirectUri, String code){ + return new KakaoTokenRequest(clientId,redirectUri,code); + } + + public String getUrlEncodedParams() { + StringBuilder params = new StringBuilder(); + + params.append("grant_type=").append(URLEncoder.encode(grantType, StandardCharsets.UTF_8)); + params.append("&client_id=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8)); + params.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, StandardCharsets.UTF_8)); + params.append("&code=").append(URLEncoder.encode(code, StandardCharsets.UTF_8)); + + return params.toString(); + } +} diff --git a/src/main/java/org/noostak/auth/dto/KakaoTokenResponse.java b/src/main/java/org/noostak/auth/dto/KakaoTokenResponse.java new file mode 100644 index 00000000..d896750e --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/KakaoTokenResponse.java @@ -0,0 +1,34 @@ +package org.noostak.auth.dto; + + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.auth.common.exception.KakaoApiErrorCode; +import org.noostak.auth.common.exception.KakaoApiException; + +@Getter +@AllArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoTokenResponse { + private String tokenType; + private String accessToken; + private String idToken; + private int expiresIn; + private String refreshToken; + private String refreshTokenExpiresIn; + private String scope; + + private String error; + private String errorDescription; + private String errorCode; + + public void validate(){ + if(error!= null){ + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_ERROR, errorDescription); + } + } +} diff --git a/src/main/java/org/noostak/auth/dto/KakaoUserInfoResponse.java b/src/main/java/org/noostak/auth/dto/KakaoUserInfoResponse.java new file mode 100644 index 00000000..150b35b2 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/KakaoUserInfoResponse.java @@ -0,0 +1,59 @@ +package org.noostak.auth.dto; + + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.noostak.auth.common.exception.KakaoApiErrorCode; +import org.noostak.auth.common.exception.KakaoApiException; + + +@Getter +@AllArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoUserInfoResponse { + private String id; + + private String connectedAt; + + private Properties properties; + + private KakaoAccount kakaoAccount; + + private String error; + private String errorDescription; + private String errorCode; + + public void validate(){ + if(error!= null){ + throw new KakaoApiException(KakaoApiErrorCode.KAKAO_API_ERROR, errorDescription); + } + } + + @Getter + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Properties { + private String nickname; + } + + + @Getter + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class KakaoAccount { + private boolean profileNicknameNeedsAgreement; + + private Profile profile; + + @Getter + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Profile { + private String nickname; + private boolean isDefaultNickname; + } + } +} + diff --git a/src/main/java/org/noostak/auth/dto/SignUpRequest.java b/src/main/java/org/noostak/auth/dto/SignUpRequest.java new file mode 100644 index 00000000..6b778c62 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/SignUpRequest.java @@ -0,0 +1,14 @@ +package org.noostak.auth.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.web.multipart.MultipartFile; + +@Getter +@AllArgsConstructor +public class SignUpRequest { + String memberName; + MultipartFile memberProfileImage; + String authType; +} diff --git a/src/main/java/org/noostak/auth/dto/SignUpResponse.java b/src/main/java/org/noostak/auth/dto/SignUpResponse.java new file mode 100644 index 00000000..e6730344 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/SignUpResponse.java @@ -0,0 +1,25 @@ +package org.noostak.auth.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.web.multipart.MultipartFile; + +@Getter +public class SignUpResponse { + private String accessToken; + private String refreshToken; + private Long memberId; + private String authType; + + private SignUpResponse(String accessToken, String refreshToken, Long memberId, String authType) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.memberId = memberId; + this.authType = authType; + } + + public static SignUpResponse of(String accessToken, String refreshToken, Long memberId, String authType){ + return new SignUpResponse(accessToken,refreshToken,memberId,authType); + } +} diff --git a/src/main/java/org/noostak/auth/dto/SocialLoginRequest.java b/src/main/java/org/noostak/auth/dto/SocialLoginRequest.java new file mode 100644 index 00000000..ef6eb9e3 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/SocialLoginRequest.java @@ -0,0 +1,11 @@ +package org.noostak.auth.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SocialLoginRequest { + private String authType; +} diff --git a/src/main/java/org/noostak/auth/dto/SocialLoginResponse.java b/src/main/java/org/noostak/auth/dto/SocialLoginResponse.java new file mode 100644 index 00000000..09e9ceaf --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/SocialLoginResponse.java @@ -0,0 +1,15 @@ +package org.noostak.auth.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SocialLoginResponse { + private String accessToken; + private String refreshToken; + private Long memberId; + private String code; + private String authType; +} diff --git a/src/main/java/org/noostak/auth/dto/TokenInfo.java b/src/main/java/org/noostak/auth/dto/TokenInfo.java new file mode 100644 index 00000000..47db8619 --- /dev/null +++ b/src/main/java/org/noostak/auth/dto/TokenInfo.java @@ -0,0 +1,19 @@ +package org.noostak.auth.dto; + + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class TokenInfo { + String authId; + + private TokenInfo(String authId){ + this.authId=authId; + } + + public static TokenInfo of(String authId){ + return new TokenInfo(authId); + } +} diff --git a/src/main/java/org/noostak/global/config/AwsConfig.java b/src/main/java/org/noostak/global/config/AwsConfig.java index 1114b5c3..00ebc82a 100644 --- a/src/main/java/org/noostak/global/config/AwsConfig.java +++ b/src/main/java/org/noostak/global/config/AwsConfig.java @@ -1,10 +1,13 @@ package org.noostak.global.config; import lombok.Getter; +import org.noostak.infra.S3Storage; +import org.noostak.infra.S3StorageImpl; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; @@ -27,22 +30,29 @@ public class AwsConfig { @Value("${aws-property.max-file-size}") private String maxFileSize; + @Bean - public Region getRegion() { + public S3Storage s3Storage(){ + return S3StorageImpl.of(S3Client(), S3BucketName(),maxFileSize()); + } + @Bean + public Region region() { return Region.of(awsRegion); } @Bean - public S3Client getS3Client() { + public S3Client S3Client() { return S3Client.builder() - .region(getRegion()) - .credentialsProvider(DefaultCredentialsProvider.create()) + .region(region()) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey) + )) .build(); } @Bean - public Long getMaxFileSize() { + public Long maxFileSize() { try { return Long.parseLong(maxFileSize); } catch (NumberFormatException e) { @@ -51,7 +61,7 @@ public Long getMaxFileSize() { } @Bean - public String getS3BucketName(){ + public String S3BucketName(){ return this.s3BucketName; } } \ No newline at end of file diff --git a/src/main/java/org/noostak/global/config/JwtInterceptor.java b/src/main/java/org/noostak/global/config/JwtInterceptor.java new file mode 100644 index 00000000..5478c847 --- /dev/null +++ b/src/main/java/org/noostak/global/config/JwtInterceptor.java @@ -0,0 +1,38 @@ +package org.noostak.global.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.noostak.auth.application.jwt.JwtTokenProvider; +import org.noostak.auth.common.exception.AuthErrorCode; +import org.noostak.auth.common.exception.AuthException; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class JwtInterceptor implements HandlerInterceptor { + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String token = extractToken(request); + + // TODO: 각 Oauth 에서 지원하는 토큰 정보 APi 호출 + if (token != null && jwtTokenProvider.validateToken(token)) { + request.setAttribute("authId", jwtTokenProvider.getAuthId(token)); + return true; + } + + throw new AuthException(AuthErrorCode.INVALID_TOKEN); + } + + private String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/org/noostak/global/config/OAuthConfig.java b/src/main/java/org/noostak/global/config/OAuthConfig.java new file mode 100644 index 00000000..60210b54 --- /dev/null +++ b/src/main/java/org/noostak/global/config/OAuthConfig.java @@ -0,0 +1,14 @@ +package org.noostak.global.config; + +import org.noostak.auth.application.RestClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OAuthConfig { + + @Bean + public RestClient restClient(){ + return new RestClient(); + } +} \ No newline at end of file diff --git a/src/main/java/org/noostak/global/config/WebConfig.java b/src/main/java/org/noostak/global/config/WebConfig.java new file mode 100644 index 00000000..970fad43 --- /dev/null +++ b/src/main/java/org/noostak/global/config/WebConfig.java @@ -0,0 +1,20 @@ +package org.noostak.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + private final JwtInterceptor jwtInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/v1/auth/**"); + } +} + diff --git a/src/main/java/org/noostak/global/error/core/BaseException.java b/src/main/java/org/noostak/global/error/core/BaseException.java index 9588af28..5efbb2b9 100644 --- a/src/main/java/org/noostak/global/error/core/BaseException.java +++ b/src/main/java/org/noostak/global/error/core/BaseException.java @@ -11,4 +11,9 @@ public BaseException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } + + public BaseException(ErrorCode errorCode, Object ... args) { + super(errorCode.getMessage(args)); + this.errorCode = errorCode; + } } diff --git a/src/main/java/org/noostak/global/error/core/ErrorResponse.java b/src/main/java/org/noostak/global/error/core/ErrorResponse.java index 12072270..29b6c613 100644 --- a/src/main/java/org/noostak/global/error/core/ErrorResponse.java +++ b/src/main/java/org/noostak/global/error/core/ErrorResponse.java @@ -13,4 +13,8 @@ public class ErrorResponse { public static ErrorResponse of(ErrorCode errorCode) { return new ErrorResponse(errorCode.getStatus(), errorCode.getMessage()); } + + public static ErrorResponse of(ErrorCode errorCode, String message) { + return new ErrorResponse(errorCode.getStatus(), message); + } } diff --git a/src/main/java/org/noostak/global/error/handler/GlobalExceptionHandler.java b/src/main/java/org/noostak/global/error/handler/GlobalExceptionHandler.java index 1b857ffc..ed84c337 100644 --- a/src/main/java/org/noostak/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/org/noostak/global/error/handler/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package org.noostak.global.error.handler; +import lombok.extern.slf4j.Slf4j; import org.noostak.global.error.core.BaseException; import org.noostak.global.error.core.ErrorResponse; import org.springframework.http.ResponseEntity; @@ -7,17 +8,21 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice +@Slf4j public class GlobalExceptionHandler { - @ExceptionHandler(BaseException.class) public ResponseEntity handleBaseException(BaseException e) { + log.error("Error: {}",e.toString()); + return ResponseEntity .status(e.getErrorCode().getStatus()) - .body(ErrorResponse.of(e.getErrorCode())); + .body(ErrorResponse.of(e.getErrorCode(),e.getMessage())); } @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntimeException(RuntimeException e) { + log.error("Error: {}",e.toString()); + return ResponseEntity .status(GlobalErrorCode.INTERNAL_SERVER_ERROR.getStatus()) .body(ErrorResponse.of(GlobalErrorCode.INTERNAL_SERVER_ERROR)); @@ -25,6 +30,8 @@ public ResponseEntity handleRuntimeException(RuntimeException e) @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { + log.error("Error: {}",e.toString()); + return ResponseEntity .status(GlobalErrorCode.INTERNAL_SERVER_ERROR.getStatus()) .body(ErrorResponse.of(GlobalErrorCode.INTERNAL_SERVER_ERROR)); diff --git a/src/main/java/org/noostak/group/application/GroupCreateServiceImpl.java b/src/main/java/org/noostak/group/application/GroupCreateServiceImpl.java index bf750b54..4bac4610 100644 --- a/src/main/java/org/noostak/group/application/GroupCreateServiceImpl.java +++ b/src/main/java/org/noostak/group/application/GroupCreateServiceImpl.java @@ -68,11 +68,7 @@ private GroupCreateInternalResponse saveGroup(Group group, String imageUrl, Stri } private KeyAndUrl uploadGroupProfileImageSafely(MultipartFile file) { - try { - return s3Service.uploadImage(S3DirectoryPath.GROUP, file); - } catch (IOException e) { - throw new GroupException(GroupErrorCode.GROUP_PROFILE_IMAGE_UPLOAD_FAILED); - } + return s3Service.uploadImage(S3DirectoryPath.GROUP, file); } private void deleteUploadedImageSafely(String key) { diff --git a/src/main/java/org/noostak/infra/DevS3ServiceImpl.java b/src/main/java/org/noostak/infra/DevS3ServiceImpl.java new file mode 100644 index 00000000..bac75dd4 --- /dev/null +++ b/src/main/java/org/noostak/infra/DevS3ServiceImpl.java @@ -0,0 +1,31 @@ +package org.noostak.infra; + +import org.noostak.infra.error.S3UploadErrorCode; +import org.noostak.infra.error.S3UploadException; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service("dev") +public class DevS3ServiceImpl implements S3Service { + + private DevS3ServiceImpl() {} + + public static DevS3ServiceImpl of() { + return new DevS3ServiceImpl(); + } + + @Override + public KeyAndUrl uploadImage(S3DirectoryPath dirPath, MultipartFile image) { + return KeyAndUrl.of(dirPath.getName(),getImageUrl(dirPath.getPath())); + } + + @Override + public String getImageUrl(String key) { + return "https://dev.aws.com/image.jpg"; + } + + @Override + public void deleteImage(String key) { + } +} diff --git a/src/main/java/org/noostak/infra/S3Service.java b/src/main/java/org/noostak/infra/S3Service.java index 2dbe1aaf..e8fb5719 100644 --- a/src/main/java/org/noostak/infra/S3Service.java +++ b/src/main/java/org/noostak/infra/S3Service.java @@ -2,10 +2,8 @@ import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - public interface S3Service { - KeyAndUrl uploadImage(S3DirectoryPath dirPath, MultipartFile image) throws IOException; + KeyAndUrl uploadImage(S3DirectoryPath dirPath, MultipartFile image); String getImageUrl(String key); void deleteImage(String key); } diff --git a/src/main/java/org/noostak/infra/S3ServiceImpl.java b/src/main/java/org/noostak/infra/S3ServiceImpl.java index 368f1778..bbd18477 100644 --- a/src/main/java/org/noostak/infra/S3ServiceImpl.java +++ b/src/main/java/org/noostak/infra/S3ServiceImpl.java @@ -1,11 +1,13 @@ package org.noostak.infra; -import org.springframework.stereotype.Component; +import org.noostak.infra.error.S3UploadErrorCode; +import org.noostak.infra.error.S3UploadException; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - -@Component +@Primary +@Service("prod") public class S3ServiceImpl implements S3Service { private final S3Storage s3Storage; @@ -19,8 +21,12 @@ public static S3ServiceImpl of(S3Storage s3Storage) { } @Override - public KeyAndUrl uploadImage(S3DirectoryPath dirPath, MultipartFile image) throws IOException { - return s3Storage.upload(dirPath, image); + public KeyAndUrl uploadImage(S3DirectoryPath dirPath, MultipartFile image) { + try { + return s3Storage.upload(dirPath, image); + } catch (Exception e) { + throw new S3UploadException(S3UploadErrorCode.IMAGE_UPLOAD_FAILED,e.getMessage()); + } } @Override diff --git a/src/main/java/org/noostak/infra/error/S3UploadErrorCode.java b/src/main/java/org/noostak/infra/error/S3UploadErrorCode.java index f4f2ecad..168f568d 100644 --- a/src/main/java/org/noostak/infra/error/S3UploadErrorCode.java +++ b/src/main/java/org/noostak/infra/error/S3UploadErrorCode.java @@ -10,7 +10,10 @@ @AllArgsConstructor public enum S3UploadErrorCode implements ErrorCode { INVALID_EXTENSION(HttpStatus.BAD_REQUEST, "이미지 확장자는 jpg, png, webp만 가능합니다."), - FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지 사이즈는 5MB를 넘을 수 없습니다."); + FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "이미지 사이즈는 5MB를 넘을 수 없습니다."), + IMAGE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "이미지 업로드에 실패하였습니다. 에러 원인: %s") + + ; public static final String PREFIX = "[S3 UPLOAD ERROR] "; diff --git a/src/main/java/org/noostak/infra/error/S3UploadException.java b/src/main/java/org/noostak/infra/error/S3UploadException.java index 85d777dc..76c49fef 100644 --- a/src/main/java/org/noostak/infra/error/S3UploadException.java +++ b/src/main/java/org/noostak/infra/error/S3UploadException.java @@ -4,4 +4,7 @@ public class S3UploadException extends RuntimeException{ public S3UploadException(S3UploadErrorCode errorCode) { super(errorCode.getMessage()); } + public S3UploadException(S3UploadErrorCode errorCode, Object ... args) { + super(errorCode.getMessage(args)); + } } \ No newline at end of file diff --git a/src/main/java/org/noostak/member/api/temp b/src/main/java/org/noostak/member/api/temp deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/org/noostak/member/application/MemberService.java b/src/main/java/org/noostak/member/application/MemberService.java new file mode 100644 index 00000000..e9f41c7d --- /dev/null +++ b/src/main/java/org/noostak/member/application/MemberService.java @@ -0,0 +1,19 @@ +package org.noostak.member.application; + +import org.noostak.member.domain.Member; +import org.noostak.auth.dto.SignUpRequest; + +public interface MemberService { + // create + Member createMember(SignUpRequest request); + + // read + void fetchMember(); + + // update + void updateMember(); + + // delete + void deleteMember(); + +} diff --git a/src/main/java/org/noostak/member/application/MemberServiceImpl.java b/src/main/java/org/noostak/member/application/MemberServiceImpl.java new file mode 100644 index 00000000..24b0ce1f --- /dev/null +++ b/src/main/java/org/noostak/member/application/MemberServiceImpl.java @@ -0,0 +1,67 @@ +package org.noostak.member.application; + +import lombok.RequiredArgsConstructor; +import org.noostak.auth.application.AuthInfoService; +import org.noostak.infra.KeyAndUrl; +import org.noostak.infra.S3DirectoryPath; +import org.noostak.infra.S3Service; +import org.noostak.member.domain.Member; +import org.noostak.member.domain.MemberRepository; +import org.noostak.member.domain.vo.MemberName; +import org.noostak.member.domain.vo.MemberProfileImageKey; +import org.noostak.auth.dto.SignUpRequest; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + + +@Service +@Transactional(readOnly = true) +public class MemberServiceImpl implements MemberService{ + + private final MemberRepository memberRepository; + + private final S3Service s3Service; + + public MemberServiceImpl(MemberRepository memberRepository, + @Qualifier("dev") S3Service s3Service) { + this.memberRepository = memberRepository; + this.s3Service = s3Service; + } + + @Override + @Transactional + public Member createMember(SignUpRequest request) { + KeyAndUrl keyAndUrl = saveProfileImage(request.getMemberProfileImage()); + Member newMember = createMember(request, keyAndUrl); + + return memberRepository.save(newMember); + } + + private Member createMember(SignUpRequest request, KeyAndUrl keyAndUrl){ + return Member.of( + MemberName.from(request.getMemberName()), + MemberProfileImageKey.from(keyAndUrl.getKey()) + ); + } + + private KeyAndUrl saveProfileImage(MultipartFile file){ + return s3Service.uploadImage(S3DirectoryPath.MEMBER,file); + } + + @Override + public void fetchMember() { + + } + + @Override + public void updateMember() { + + } + + @Override + public void deleteMember() { + + } +} diff --git a/src/main/java/org/noostak/member/application/temp b/src/main/java/org/noostak/member/application/temp deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/org/noostak/member/common/MemberErrorCode.java b/src/main/java/org/noostak/member/common/MemberErrorCode.java index a124db09..32d07455 100644 --- a/src/main/java/org/noostak/member/common/MemberErrorCode.java +++ b/src/main/java/org/noostak/member/common/MemberErrorCode.java @@ -16,7 +16,7 @@ public enum MemberErrorCode implements ErrorCode { AUTH_ID_NOT_EMPTY(HttpStatus.BAD_REQUEST, "인증 ID는 비어 있을 수 없습니다."), AUTH_ID_NOT_NULL(HttpStatus.BAD_REQUEST, "인증 ID는 null일 수 없습니다."), - + MEMBER_PROFILE_UPLOAD_FAIL(HttpStatus.BAD_REQUEST, "멤버 이미지 업로드에 실패하였습니다."), ; diff --git a/src/main/java/org/noostak/member/domain/Member.java b/src/main/java/org/noostak/member/domain/Member.java index 99b70839..b3b156f2 100644 --- a/src/main/java/org/noostak/member/domain/Member.java +++ b/src/main/java/org/noostak/member/domain/Member.java @@ -27,26 +27,13 @@ public class Member extends BaseTimeEntity { @Column(name = "member_account_status") private MemberAccountStatus status; - @Enumerated(EnumType.STRING) - @Column(name = "auth_type") - private AuthType type; - - @Embedded - @AttributeOverride(name = "id", column = @Column(name = "auth_id")) - private AuthId authId; - - private String refreshToken; - - private Member(final MemberName name, final MemberProfileImageKey key, final AuthType type, final AuthId authId, final String refreshToken) { + private Member(final MemberName name, final MemberProfileImageKey key) { this.name = name; this.key = key; this.status = MemberAccountStatus.ACTIVE; - this.type = type; - this.authId = authId; - this.refreshToken = refreshToken; } - - public static Member of(final MemberName name, final MemberProfileImageKey key, final AuthType type, final AuthId authId, final String refreshToken) { - return new Member(name, key, type, authId, refreshToken); + + public static Member of(final MemberName name, final MemberProfileImageKey key) { + return new Member(name, key); } } diff --git a/src/main/java/org/noostak/member/domain/vo/AuthType.java b/src/main/java/org/noostak/member/domain/vo/AuthType.java deleted file mode 100644 index 8ae34fd6..00000000 --- a/src/main/java/org/noostak/member/domain/vo/AuthType.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.noostak.member.domain.vo; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum AuthType { - KAKAO("카카오"), - GOOGLE("구글"), - APPLE("애플"); - - private final String message; -} diff --git a/src/main/java/org/noostak/member/dto/temp b/src/main/java/org/noostak/member/dto/temp deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/org/noostak/auth/application/KakaoServiceImplTest.java b/src/test/java/org/noostak/auth/application/KakaoServiceImplTest.java new file mode 100644 index 00000000..70c2b622 --- /dev/null +++ b/src/test/java/org/noostak/auth/application/KakaoServiceImplTest.java @@ -0,0 +1,170 @@ +package org.noostak.auth.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.noostak.auth.application.jwt.JwtToken; +import org.noostak.auth.application.jwt.JwtTokenProvider; +import org.noostak.auth.common.exception.KakaoApiErrorCode; +import org.noostak.auth.domain.vo.AuthId; +import org.noostak.auth.dto.*; +import org.springframework.http.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class KakaoServiceImplTest { + + @Mock + private KakaoTokenRequestFactory kakaoTokenRequestFactory; + + @Mock + private RestClient restClient; + + @InjectMocks + private KakaoServiceImpl kakaoService; + + private final String MOCK_CODE = "testCode"; + private final String MOCK_ACCESS_TOKEN = "testAccessToken"; + private final String MOCK_REFRESH_TOKEN = "testRefreshToken"; + private final String MOCK_KAKAO_ID = "12345"; + + private final String ERROR_MSG = KakaoApiErrorCode.KAKAO_API_ERROR.getMessage(); + + @BeforeEach + void setUp() { + // 테스트 준비 + } + + @Nested + @DisplayName("성공 케이스") + class Success { + @Test + @DisplayName("토큰 정보 조회 테스트") + void fetchTokenInfoTest() { + // given + KakaoTokenInfoResponse mockResponse = mock(KakaoTokenInfoResponse.class); + when(mockResponse.getId()).thenReturn(MOCK_KAKAO_ID); + doNothing().when(mockResponse).validate(); + + when(restClient.postRequest( + eq(KaKaoApi.FETCH_TOKEN.getUrl()), + any(HttpHeaders.class), + eq(KakaoTokenInfoResponse.class))) + .thenReturn(mockResponse); + + try (MockedStatic mockedStatic = mockStatic(TokenInfo.class)) { + TokenInfo expectedTokenInfo = mock(TokenInfo.class); + mockedStatic.when(() -> TokenInfo.of(MOCK_KAKAO_ID)).thenReturn(expectedTokenInfo); + + // when + TokenInfo result = kakaoService.fetchTokenInfo(MOCK_ACCESS_TOKEN); + + // then + assertThat(result).isEqualTo(expectedTokenInfo); + + verify(mockResponse).validate(); + verify(restClient).postRequest( + eq(KaKaoApi.FETCH_TOKEN.getUrl()), + any(HttpHeaders.class), + eq(KakaoTokenInfoResponse.class)); + } + } + + @Test + @DisplayName("토큰 요청 테스트") + void requestTokenTest() { + // given + KakaoTokenRequest mockRequest = mock(KakaoTokenRequest.class); + KakaoTokenResponse mockResponse = mock(KakaoTokenResponse.class); + + when(mockRequest.getUrlEncodedParams()).thenReturn("code=testCode"); + when(mockResponse.getAccessToken()).thenReturn(MOCK_ACCESS_TOKEN); + when(mockResponse.getRefreshToken()).thenReturn(MOCK_REFRESH_TOKEN); + doNothing().when(mockResponse).validate(); + + when(kakaoTokenRequestFactory.createRequest(MOCK_CODE)).thenReturn(mockRequest); + when(restClient.postRequest( + eq(KaKaoApi.TOKEN_REQUEST.getUrl()), + anyString(), + eq(KakaoTokenResponse.class))) + .thenReturn(mockResponse); + + JwtToken expectedToken = new JwtToken("Bearer", MOCK_ACCESS_TOKEN, MOCK_REFRESH_TOKEN); + + try (MockedStatic mockedStatic = mockStatic(JwtTokenProvider.class)) { + mockedStatic.when(() -> JwtTokenProvider.createToken(MOCK_ACCESS_TOKEN, MOCK_REFRESH_TOKEN)) + .thenReturn(expectedToken); + + // when + JwtToken result = kakaoService.requestToken(MOCK_CODE); + + // then + assertThat(result).isEqualTo(expectedToken); + + verify(mockResponse).validate(); + verify(kakaoTokenRequestFactory).createRequest(MOCK_CODE); + verify(restClient).postRequest( + eq(KaKaoApi.TOKEN_REQUEST.getUrl()), + anyString(), + eq(KakaoTokenResponse.class)); + } + } + + @Test + @DisplayName("로그인 테스트") + void loginTest() { + // given + KakaoUserInfoResponse mockResponse = mock(KakaoUserInfoResponse.class); + when(mockResponse.getId()).thenReturn(MOCK_KAKAO_ID); + doNothing().when(mockResponse).validate(); + + AuthId expectedAuthId = AuthId.from(MOCK_KAKAO_ID); + + when(restClient.postRequest( + eq(KaKaoApi.USER_INFO.getUrl()), + any(HttpHeaders.class), + eq(KakaoUserInfoResponse.class))) + .thenReturn(mockResponse); + + try (MockedStatic mockedStatic = mockStatic(AuthId.class)) { + mockedStatic.when(() -> AuthId.from(MOCK_KAKAO_ID)).thenReturn(expectedAuthId); + + // when + AuthId result = kakaoService.login(MOCK_ACCESS_TOKEN); + + // then + assertThat(result).isEqualTo(expectedAuthId); + + verify(mockResponse).validate(); + verify(restClient).postRequest( + eq(KaKaoApi.USER_INFO.getUrl()), + any(HttpHeaders.class), + eq(KakaoUserInfoResponse.class)); + } + } + + @Test + @DisplayName("인증 헤더 생성 테스트") + void makeAuthorizationBearerTokenHeaderTest() { + // given + String token = "testToken"; + + // when + HttpHeaders headers = kakaoService.makeAuthorizationBearerTokenHeader(token); + + // then + assertThat(headers).isNotNull(); + assertThat(headers.getFirst("Authorization")).isEqualTo("Bearer " + token); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/noostak/group/application/GroupCreateServiceImplTest.java b/src/test/java/org/noostak/group/application/GroupCreateServiceImplTest.java index 52c4fc8a..ff59ddb4 100644 --- a/src/test/java/org/noostak/group/application/GroupCreateServiceImplTest.java +++ b/src/test/java/org/noostak/group/application/GroupCreateServiceImplTest.java @@ -20,8 +20,6 @@ import org.noostak.infra.S3Service; import org.noostak.member.domain.Member; import org.noostak.member.domain.MemberRepository; -import org.noostak.member.domain.vo.AuthId; -import org.noostak.member.domain.vo.AuthType; import org.noostak.member.domain.vo.MemberName; import org.noostak.member.domain.vo.MemberProfileImageKey; import org.noostak.member.MemberRepositoryTest; @@ -60,7 +58,7 @@ void setUp() throws IOException { groupRepository.deleteAll(); memberRepository.deleteAll(); - Member savedMember = saveMember("jsoonworld", "key", "123456", "refreshToken1"); + Member savedMember = saveMember("jsoonworld", "key"); savedMemberId = savedMember.getId(); Mockito.when(invitationCodeGenerator.generate()).thenReturn(GroupInvitationCode.from("ABC123")); @@ -117,14 +115,11 @@ void shouldFailToCreateGroupWhenUserDoesNotExist() { } } - private Member saveMember(String name, String key, String authId, String refreshToken) { + private Member saveMember(String name, String key) { return memberRepository.save( Member.of( MemberName.from(name), - MemberProfileImageKey.from(key), - AuthType.GOOGLE, - AuthId.from(authId), - refreshToken + MemberProfileImageKey.from(key) ) ); } diff --git a/src/test/java/org/noostak/group/application/GroupRetrieveServiceImplTest.java b/src/test/java/org/noostak/group/application/GroupRetrieveServiceImplTest.java index 67c4d7d4..7ec8132e 100644 --- a/src/test/java/org/noostak/group/application/GroupRetrieveServiceImplTest.java +++ b/src/test/java/org/noostak/group/application/GroupRetrieveServiceImplTest.java @@ -16,8 +16,6 @@ import org.noostak.member.MemberRepositoryTest; import org.noostak.member.domain.Member; import org.noostak.member.domain.MemberRepository; -import org.noostak.member.domain.vo.AuthId; -import org.noostak.member.domain.vo.AuthType; import org.noostak.member.domain.vo.MemberName; import org.noostak.member.domain.vo.MemberProfileImageKey; import org.noostak.membergroup.MemberGroupRepositoryTest; @@ -51,7 +49,7 @@ void setUp() { groupRetrieveService = new GroupRetrieveServiceImpl(memberGroupRepository, s3Service); - Member savedMember = saveMember("MemberOne", "key1", "authId1", "refreshToken1"); + Member savedMember = saveMember("MemberOne", "key1"); savedMemberId = savedMember.getId(); Group savedGroup1 = saveGroup(savedMemberId, "StudyGroup", "group-images/1", "ABC123"); @@ -77,7 +75,7 @@ void shouldRetrieveMultipleGroupsSuccessfully() { // given Long memberId = savedMemberId; - List memberGroups = memberGroupRepository.findByMemberId(memberId); + List memberGroups = memberGroupRepository.findByMember_MemberId(memberId); assertThat(memberGroups).isNotEmpty(); // when @@ -92,8 +90,9 @@ void shouldRetrieveMultipleGroupsSuccessfully() { @DisplayName("멤버가 하나의 그룹만 속해 있는 경우 정상 조회") void shouldRetrieveSingleGroupSuccessfully() { // given - Long memberId = saveMember("singleUser", "keySingle", "authIdSingle", "refreshTokenSingle").getId(); + Long memberId = saveMember("singleUser", "keySingle").getId(); Long groupId = saveGroup(memberId, "SingleGroup", "group-images/single", "SINGLE").getId(); + saveMemberGroup(memberId, groupId); // when @@ -108,8 +107,9 @@ void shouldRetrieveSingleGroupSuccessfully() { @DisplayName("여러 명의 멤버가 같은 그룹에 속한 경우 특정 멤버가 정상적으로 조회") void shouldRetrieveGroupsWhenMultipleMembersInSameGroup() { // given - Long member1 = saveMember("memberOne", "key1", "authId1", "refreshToken1").getId(); - Long member2 = saveMember("memberTwo", "key2", "authId2", "refreshToken2").getId(); + + Long member1 = saveMember("memberOne", "key1").getId(); + Long member2 = saveMember("memberTwo", "key2").getId(); Long groupId = saveGroup(member1, "SharedGroup", "group-images/shared", "SHARED").getId(); saveMemberGroup(member1, groupId); @@ -175,14 +175,11 @@ private Group saveGroup(Long groupHostId, String groupName, String groupImageUrl ); } - private Member saveMember(String name, String key, String authId, String refreshToken) { + private Member saveMember(String name, String key) { return memberRepository.save( Member.of( MemberName.from(name), - MemberProfileImageKey.from(key), - AuthType.GOOGLE, - AuthId.from(authId), - refreshToken + MemberProfileImageKey.from(key) ) ); } diff --git a/src/test/java/org/noostak/member/domain/MemberServiceImplTest.java b/src/test/java/org/noostak/member/domain/MemberServiceImplTest.java new file mode 100644 index 00000000..36bad660 --- /dev/null +++ b/src/test/java/org/noostak/member/domain/MemberServiceImplTest.java @@ -0,0 +1,76 @@ +package org.noostak.member.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 org.noostak.auth.dto.SignUpRequest; +import org.noostak.infra.KeyAndUrl; +import org.noostak.infra.S3DirectoryPath; +import org.noostak.infra.S3Service; +import org.noostak.member.application.MemberServiceImpl; +import org.springframework.web.multipart.MultipartFile; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MemberServiceImplTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private S3Service s3Service; + + @InjectMocks + private MemberServiceImpl memberService; + + @Nested + @DisplayName("멤버 생성 성공 케이스") + class Success { + @Test + @DisplayName("정상적인 회원 가입 요청 시 멤버가 성공적으로 생성된다") + void createMember_Success() { + // given + SignUpRequest request = mock(SignUpRequest.class); + MultipartFile mockFile = mock(MultipartFile.class); + when(request.getMemberProfileImage()).thenReturn(mockFile); + when(request.getMemberName()).thenReturn("홍길동"); + when(s3Service.uploadImage(eq(S3DirectoryPath.MEMBER), any(MultipartFile.class))) + .thenReturn(KeyAndUrl.of("profile-key", "profile-url")); + when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + Member createdMember = memberService.createMember(request); + + // then + assertNotNull(createdMember); + assertEquals("홍길동", createdMember.getName().value()); + assertEquals("profile-key", createdMember.getKey().value()); + } + } + + @Nested + @DisplayName("멤버 생성 실패 케이스") + class Failure { + @Test + @DisplayName("프로필 이미지 업로드 실패 시 예외 발생") + void createMember_Fail_WhenProfileUploadFails() { + // given + SignUpRequest request = mock(SignUpRequest.class); + MultipartFile mockFile = mock(MultipartFile.class); + when(request.getMemberProfileImage()).thenReturn(mockFile); + when(s3Service.uploadImage(eq(S3DirectoryPath.MEMBER), any(MultipartFile.class))) + .thenThrow(new RuntimeException("S3 업로드 실패")); + + // when & then + assertThrows(RuntimeException.class, () -> memberService.createMember(request)); + } + } +} + diff --git a/src/test/java/org/noostak/member/domain/vo/AuthIdTest.java b/src/test/java/org/noostak/member/domain/vo/AuthIdTest.java index db779081..129ed9c2 100644 --- a/src/test/java/org/noostak/member/domain/vo/AuthIdTest.java +++ b/src/test/java/org/noostak/member/domain/vo/AuthIdTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.NullSource; +import org.noostak.auth.domain.vo.AuthId; import org.noostak.member.common.MemberErrorCode; import org.noostak.member.common.MemberException; @@ -29,7 +30,7 @@ class SuccessCases { "unique.auth.id", "simpleid" }) - void shouldCreateAuthIdSuccessfully(String validId) { + void shouldCreateCodeSuccessfully(String validId) { // Given & When AuthId authId = AuthId.from(validId); diff --git a/src/test/java/org/noostak/membergroup/MemberGroupRepositoryTest.java b/src/test/java/org/noostak/membergroup/MemberGroupRepositoryTest.java index 3a5048e0..c26e10ec 100644 --- a/src/test/java/org/noostak/membergroup/MemberGroupRepositoryTest.java +++ b/src/test/java/org/noostak/membergroup/MemberGroupRepositoryTest.java @@ -50,7 +50,7 @@ public Optional findById(Long id) { } @Override - public List findByMemberId(Long memberId) { + public List findByMember_MemberId(Long memberId) { return memberGroups.stream() .filter(memberGroup -> memberGroup.getMember().getId().equals(memberId)) .toList();