Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat(#50): 안드로이드 전용 카카오 로그인 API #54

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.nexters.jaknaesocore.domain.auth.service;

import static org.nexters.jaknaesocore.domain.auth.restclient.dto.KakaoUserInfoResponse.KakaoAccount;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,6 +15,7 @@
import org.nexters.jaknaesocore.domain.auth.restclient.dto.KakaoUserInfoResponse;
import org.nexters.jaknaesocore.domain.auth.service.dto.AppleLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginWithTokenCommand;
import org.nexters.jaknaesocore.domain.member.model.Member;
import org.nexters.jaknaesocore.domain.member.repository.MemberRepository;
import org.nexters.jaknaesocore.domain.socialaccount.model.SocialAccount;
Expand Down Expand Up @@ -50,18 +53,15 @@ public Long kakaoLogin(final KakaoLoginCommand command) {
final KakaoUserInfoResponse userInfo = getKakaoUserInfo(token.accessToken());

final String oauthId = userInfo.id().toString();
return socialAccountRepository
.findByOauthIdAndSocialProviderAndDeletedAtIsNull(oauthId, SocialProvider.KAKAO)
.map(SocialAccount::getMember)
.map(
it -> {
it.updateUserInfo(userInfo.kakaoAccount().name(), userInfo.kakaoAccount().email());
return it.getId();
})
.orElseGet(
() ->
kakaoSignUp(
oauthId, userInfo.kakaoAccount().name(), userInfo.kakaoAccount().email()));
return kakaoSignInOrSignUp(oauthId, userInfo.kakaoAccount());
}

@Transactional
public Long kakaoLoginWithToken(final KakaoLoginWithTokenCommand command) {
final KakaoUserInfoResponse userInfo = getKakaoUserInfo(command.accessToken());

final String oauthId = userInfo.id().toString();
return kakaoSignInOrSignUp(oauthId, userInfo.kakaoAccount());
}

private KakaoUserInfoResponse getKakaoUserInfo(final String accessToken) {
Expand All @@ -79,6 +79,18 @@ private KakaoTokenResponse getKakaoToken(
return kakaoAuthClient.requestToken(params);
}

private Long kakaoSignInOrSignUp(final String oauthId, final KakaoAccount kakaoAccount) {
return socialAccountRepository
.findByOauthIdAndSocialProviderAndDeletedAtIsNull(oauthId, SocialProvider.KAKAO)
.map(SocialAccount::getMember)
.map(
it -> {
it.updateUserInfo(kakaoAccount.name(), kakaoAccount.email());
return it.getId();
})
.orElseGet(() -> kakaoSignUp(oauthId, kakaoAccount.name(), kakaoAccount.email()));
}

private Long kakaoSignUp(final String oauthId, final String name, final String email) {
final Member member = memberRepository.save(Member.create(name, email));
socialAccountRepository.save(SocialAccount.kakaoSignup(oauthId, member));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.nexters.jaknaesocore.domain.auth.service.dto;

public record KakaoLoginWithTokenCommand(String accessToken) {}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.nexters.jaknaesocore.domain.auth.restclient.dto.KakaoUserInfoResponse.KakaoAccount;
import org.nexters.jaknaesocore.domain.auth.service.dto.AppleLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginWithTokenCommand;
import org.nexters.jaknaesocore.domain.member.model.Member;
import org.nexters.jaknaesocore.domain.member.repository.MemberRepository;
import org.nexters.jaknaesocore.domain.socialaccount.model.SocialAccount;
Expand Down Expand Up @@ -55,6 +56,10 @@ private KakaoLoginCommand createKakaoLoginCommand(String authorizationCode, Stri
return new KakaoLoginCommand(authorizationCode, redirectUri);
}

private KakaoLoginWithTokenCommand createKakaoLoginWithTokenCommand(String accessToken) {
return new KakaoLoginWithTokenCommand(accessToken);
}

@Nested
@DisplayName("appleLogin 메소드는 ")
class appleLogin {
Expand Down Expand Up @@ -156,4 +161,57 @@ void shouldSignIn() {
}
}
}

@Nested
@DisplayName("kakaoLoginWithToken 메소드는")
class kakaoLoginToken {

@BeforeEach
void setUp() {
given(kakaoClient.requestUserInfo("Bearer access token"))
.willReturn(new KakaoUserInfoResponse(1L, new KakaoAccount("홍길동", "[email protected]")));
}

@Nested
@DisplayName("유저를 찾지 못하면")
class whenMemberNotFound {

@Test
@DisplayName("회원가입을 진행한다.")
void shouldSignUp() {
sut.kakaoLoginWithToken(createKakaoLoginWithTokenCommand("access token"));

assertAll(
() ->
then(memberRepository.findAll())
.hasSize(1)
.extracting("name", "email")
.containsExactly(tuple("홍길동", "[email protected]")),
() ->
then(socialAccountRepository.findAll())
.hasSize(1)
.extracting("oauthId", "socialProvider")
.containsExactlyInAnyOrder(tuple("1", SocialProvider.KAKAO)));
}
}

@Nested
@DisplayName("유저를 찾으면")
class whenMemberFound {

@Test
@DisplayName("로그인을 진행한다.")
void shouldSignIn() {
final Member member = memberRepository.save(Member.create("홍길동", "[email protected]"));
final String oauthId = "1";
socialAccountRepository.save(createSocialAccount(member, oauthId, SocialProvider.KAKAO));

sut.kakaoLoginWithToken(createKakaoLoginWithTokenCommand("access token"));

assertAll(
() -> then(memberRepository.findAll()).hasSize(1),
() -> then(socialAccountRepository.findAll()).hasSize(1));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.nexters.jaknaesocore.common.support.response.ApiResponse;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.AppleLoginRequest;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.KakaoLoginRequest;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.KakaoLoginWithTokenRequest;
import org.nexters.jaknaesoserver.domain.auth.dto.TokenResponse;
import org.nexters.jaknaesoserver.domain.auth.service.AuthFacadeService;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -33,6 +34,13 @@ public ApiResponse<TokenResponse> kakaoLogin(@RequestBody @Valid KakaoLoginReque
return ApiResponse.success(response);
}

@PostMapping("/kakao-login/token")
public ApiResponse<TokenResponse> kakaoLoginWithToken(
@RequestBody @Valid KakaoLoginWithTokenRequest request) {
TokenResponse response = authFacadeService.kakaoLoginWithToken(request.toServiceDto());
return ApiResponse.success(response);
}

@PostMapping("/reissue")
public ApiResponse<TokenResponse> refreshToken(
@RequestHeader(value = "Refresh-Token") final String bearerRefreshToken) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.nexters.jaknaesoserver.domain.auth.controller.dto;

import org.apache.logging.log4j.core.config.plugins.validation.constraints.NotBlank;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginWithTokenCommand;

public record KakaoLoginWithTokenRequest(@NotBlank String accessToken) {

public KakaoLoginWithTokenCommand toServiceDto() {
return new KakaoLoginWithTokenCommand(accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.nexters.jaknaesocore.domain.auth.service.OauthService;
import org.nexters.jaknaesocore.domain.auth.service.dto.AppleLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginCommand;
import org.nexters.jaknaesocore.domain.auth.service.dto.KakaoLoginWithTokenCommand;
import org.nexters.jaknaesoserver.domain.auth.dto.TokenResponse;
import org.springframework.stereotype.Service;

Expand All @@ -19,8 +20,13 @@ public TokenResponse appleLogin(final AppleLoginCommand command) {
return jwtService.issueToken(memberId);
}

public TokenResponse kakaoLogin(final KakaoLoginCommand kakaoLoginCommand) {
Long memberId = oauthService.kakaoLogin(kakaoLoginCommand);
public TokenResponse kakaoLogin(final KakaoLoginCommand command) {
Long memberId = oauthService.kakaoLogin(command);
return jwtService.issueToken(memberId);
}

public TokenResponse kakaoLoginWithToken(final KakaoLoginWithTokenCommand command) {
Long memberId = oauthService.kakaoLoginWithToken(command);
return jwtService.issueToken(memberId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import org.nexters.jaknaesoserver.common.support.ControllerTest;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.AppleLoginRequest;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.KakaoLoginRequest;
import org.nexters.jaknaesoserver.domain.auth.controller.dto.KakaoLoginWithTokenRequest;
import org.nexters.jaknaesoserver.domain.auth.dto.TokenResponse;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.web.client.RestClientException;

class AuthControllerTest extends ControllerTest {

Expand Down Expand Up @@ -72,7 +72,7 @@ class AuthControllerTest extends ControllerTest {
}

@Test
void 카카오_API를_호출하여_서비스에_로그인한다() throws Exception {
void 인가_코드로_카카오_API를_호출하여_서비스에_로그인한다() throws Exception {
KakaoLoginRequest request = new KakaoLoginRequest("authorization code", "redirect-uri");

given(authFacadeService.kakaoLogin(request.toServiceDto()))
Expand Down Expand Up @@ -118,44 +118,47 @@ class AuthControllerTest extends ControllerTest {
}

@Test
void 카카오_API_호출이_실패하여_서비스_로그인에_실패하고_서버_예외를_반환한다() throws Exception {
KakaoLoginRequest request = new KakaoLoginRequest("invalid authorization code", "redirect-uri");
void 토큰으로_카카오_API를_호출하여_서비스에_로그인한다() throws Exception {
KakaoLoginWithTokenRequest request = new KakaoLoginWithTokenRequest("access token");

given(authFacadeService.kakaoLogin(request.toServiceDto()))
.willThrow(RestClientException.class);
given(authFacadeService.kakaoLoginWithToken(request.toServiceDto()))
.willReturn(new TokenResponse(1L, "accessToken", "refreshToken"));

mockMvc
.perform(
post("/api/v1/auth/kakao-login")
post("/api/v1/auth/kakao-login/token")
.accept(APPLICATION_JSON)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.with(csrf()))
.andExpect(status().is5xxServerError())
.andExpect(status().isOk())
.andDo(
document(
"kakao-login-fail",
"kakao-login-with-token-success",
resource(
ResourceSnippetParameters.builder()
.description("카카오 로그인 및 토큰 발급")
.description("카카오 로그인 (for 안드로이드)")
.tags("Auth Domain")
.requestFields(
fieldWithPath("code").type(SimpleType.STRING).description("카카오 인증 코드"),
fieldWithPath("redirectUri")
fieldWithPath("accessToken")
.type(SimpleType.STRING)
.description("카카오 로그인 리다이렉트 URI"))
.description("카카오 액세스 토큰"))
.responseFields(
fieldWithPath("result")
.type(SimpleType.STRING)
.description("API 요청 결과 (성공/실패)"),
fieldWithPath("error.code")
fieldWithPath("data.memberId")
.type(SimpleType.NUMBER)
.description("유저 ID"),
fieldWithPath("data.accessToken")
.type(SimpleType.STRING)
.description("커스텀 에러 코드"),
fieldWithPath("error.message")
.description("액세스 토큰"),
fieldWithPath("data.refreshToken")
.type(SimpleType.STRING)
.description("에러 상세 메시지"),
fieldWithPath("error.data").description("에러 관련 데이터").optional(),
fieldWithPath("data").description("정상적인 요청의 결과 데이터").optional())
.description("리프레시 토큰"),
fieldWithPath("error").description("에러").optional())
.requestSchema(schema("KakaoLoginWithTokenRequest"))
.responseSchema(schema("TokenResponse"))
.build())));
}

Expand Down