diff --git a/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java b/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java index 1c3ef76b..594e7557 100644 --- a/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java @@ -1,30 +1,36 @@ package umc.th.juinjang.api.auth.controller; +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import io.micrometer.common.lang.Nullable; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.util.StringUtils; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import umc.th.juinjang.api.dto.ApiResponse; -import umc.th.juinjang.common.ExceptionHandler; -import umc.th.juinjang.common.code.status.SuccessStatus; -import umc.th.juinjang.api.auth.service.response.LoginResponseDto; -import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; -import umc.th.juinjang.api.auth.controller.request.WithdrawReasonRequestDto; import umc.th.juinjang.api.auth.controller.request.AppleLoginRequestDto; import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestDto; import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestVersion2Dto; import umc.th.juinjang.api.auth.controller.request.KakaoLoginRequestDto; import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestDto; import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestVersion2Dto; -import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.api.auth.controller.request.WithdrawReasonRequestDto; +import umc.th.juinjang.api.auth.service.OAuthServiceV2; import umc.th.juinjang.api.auth.service.WithdrawService; -import umc.th.juinjang.api.auth.service.OAuthService; - -import static umc.th.juinjang.common.code.status.ErrorStatus.*; +import umc.th.juinjang.api.auth.service.response.LoginResponseDto; +import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.domain.member.model.Member; @Slf4j @RestController @@ -33,165 +39,174 @@ @Validated public class OAuthController { - private final OAuthService oauthService; - private final WithdrawService withdrawService; - - // 카카오 로그인 - // 프론트 측에서 전달해준 사용자 정보로 토큰 발급 - @PostMapping("/kakao/login") - public ApiResponse kakaoLogin(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoLogin(targetId, kakaoReqDto)); - } - - // 카카오 로그인 (회원가입) - @PostMapping("/kakao/signup") - public ApiResponse kakaoSignUp(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoSignUpRequestDto kakaoSignUpReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoSignUp(targetId, kakaoSignUpReqDto)); - } - - //V2 - // 카카오 로그인 - @PostMapping("/v2/kakao/login") - public ApiResponse kakaoLoginVersion2(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoLoginVersion2(targetId, kakaoReqDto)); - } - - // 카카오 로그인 (회원가입) - @PostMapping("/v2/kakao/signup") - public ApiResponse kakaoSignUpVersion2(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoSignUpVersion2(targetId, kakaoSignUpReqDto)); - } - - - // refreshToken으로 accessToken 재발급 - // Authorization : Bearer Token에 refreshToken 담기 - @PostMapping("/regenerate-token") - public ApiResponse regenerateAccessToken(HttpServletRequest request) { - String accessToken = request.getHeader("Authorization"); - String refreshToken = request.getHeader("Refresh-Token"); - - if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ") && StringUtils.hasText(refreshToken) && refreshToken.startsWith("Bearer ")) { - LoginResponseDto result = oauthService.regenerateAccessToken(accessToken.substring(7), refreshToken.substring(7)); - return ApiResponse.onSuccess(result); - } else - throw new ExceptionHandler(TOKEN_EMPTY); - } - - // 로그아웃 -> refresh 토큰 만료 - @PostMapping("/logout") - public ApiResponse logout(HttpServletRequest request) { - String token = request.getHeader("Refresh-Token"); - - if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { - String result = oauthService.logout(token.substring(7)); - return ApiResponse.onSuccess(result); - } else - throw new ExceptionHandler(TOKEN_EMPTY); - } - - // 애플 로그인 - // 클라이언트에서 identity token 값 받아오기 - // 사용자가 입력한 정보를 바탕으로 Apple ID servers 에게 Identity Token 발급 요청 (프론트가) -> 이를 우리 서버가 가져오는 것 - // Identity Token 값을 바탕으로 사용자 식별 & refresh, access Token 발급해주고 DB 저장 (로그인하기) - - // 로그인 - @PostMapping("/apple/login") - public ApiResponse appleLogin(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { - if (appleReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleLogin(appleReqDto)); - } - - @PostMapping("/apple/signup") - public ApiResponse appleSignUp(@RequestBody @Validated AppleSignUpRequestDto appleSignUpReqDto) { - if (appleSignUpReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleSignUp(appleSignUpReqDto)); - } - - //V2 - @PostMapping("/v2/apple/login") - public ApiResponse appleLoginVersion2(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { - if (appleReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleLoginVersion2(appleReqDto)); - } - - @PostMapping("/v2/apple/signup") - public ApiResponse appleSignUpVersion2(@RequestBody @Validated AppleSignUpRequestVersion2Dto appleSignUpReqDto) { - if (appleSignUpReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleSignUpVersion2(appleSignUpReqDto)); - } - - - // 카카오 탈퇴 - @DeleteMapping("/withdraw/kakao") - public ApiResponse kakaoWithdraw(@AuthenticationPrincipal Member member, @RequestHeader("target-id") String kakaoTargetId, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { - Long targetId; - - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } else { - targetId = Long.parseLong(kakaoTargetId); - if(!targetId.equals(member.getKakaoTargetId())) { - throw new ExceptionHandler(UNCORRECTED_TARGET_ID); - } - } - - // 카카오 계정 연결 끊기 - boolean isUnlink = oauthService.kakaoWithdraw(member, targetId); - - // 탈퇴 사유 추가 - if(withdrawReasonReqDto.getWithdrawReason() != null) { - withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); - } - - // 사용자 정보 삭제 (DB) - if (!isUnlink) { - throw new ExceptionHandler(NOT_UNLINK_KAKAO); - } - - return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); - } - - - // 애플 탈퇴 - @DeleteMapping("/withdraw/apple") - public ApiResponse withdraw(@AuthenticationPrincipal Member member, - @Nullable@RequestHeader("X-Apple-Code") final String code, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto){ - oauthService.appleWithdraw(member, code); - - // 탈퇴 사유 추가 - if(withdrawReasonReqDto.getWithdrawReason() != null) { - withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); - } - - return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); - } - -} \ No newline at end of file + // private final OAuthService oauthService; + private final OAuthServiceV2 oauthService; + private final WithdrawService withdrawService; + + // 카카오 로그인 + // 프론트 측에서 전달해준 사용자 정보로 토큰 발급 + @PostMapping("/kakao/login") + public ApiResponse kakaoLogin(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoLogin(targetId, kakaoReqDto)); + } + + // 카카오 로그인 (회원가입) + @PostMapping("/kakao/signup") + public ApiResponse kakaoSignUp(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoSignUpRequestDto kakaoSignUpReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoSignUp(targetId, kakaoSignUpReqDto)); + } + + //V2 + // 카카오 로그인 + @PostMapping("/v2/kakao/login") + public ApiResponse kakaoLoginVersion2(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoLoginVersion2(targetId, kakaoReqDto)); + } + + // 카카오 로그인 (회원가입) + @PostMapping("/v2/kakao/signup") + public ApiResponse kakaoSignUpVersion2(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoSignUpVersion2(targetId, kakaoSignUpReqDto)); + } + + // refreshToken으로 accessToken 재발급 + // Authorization : Bearer Token에 refreshToken 담기 + @PostMapping("/regenerate-token") + public ApiResponse regenerateAccessToken(HttpServletRequest request) { + String accessToken = request.getHeader("Authorization"); + String refreshToken = request.getHeader("Refresh-Token"); + + if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ") && StringUtils.hasText(refreshToken) + && refreshToken.startsWith("Bearer ")) { + LoginResponseDto result = oauthService.regenerateAccessToken(accessToken.substring(7), + refreshToken.substring(7)); + return ApiResponse.onSuccess(result); + } else + throw new ExceptionHandler(TOKEN_EMPTY); + } + + // 로그아웃 -> refresh 토큰 만료 + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + String token = request.getHeader("Refresh-Token"); + + if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { + String result = oauthService.logout(token.substring(7)); + return ApiResponse.onSuccess(result); + } else + throw new ExceptionHandler(TOKEN_EMPTY); + } + + // 애플 로그인 + // 클라이언트에서 identity token 값 받아오기 + // 사용자가 입력한 정보를 바탕으로 Apple ID servers 에게 Identity Token 발급 요청 (프론트가) -> 이를 우리 서버가 가져오는 것 + // Identity Token 값을 바탕으로 사용자 식별 & refresh, access Token 발급해주고 DB 저장 (로그인하기) + + // 로그인 + @PostMapping("/apple/login") + public ApiResponse appleLogin(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { + if (appleReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleLogin(appleReqDto)); + } + + @PostMapping("/apple/signup") + public ApiResponse appleSignUp(@RequestBody @Validated AppleSignUpRequestDto appleSignUpReqDto) { + if (appleSignUpReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleSignUp(appleSignUpReqDto)); + } + + //V2 + @PostMapping("/v2/apple/login") + public ApiResponse appleLoginVersion2( + @RequestBody @Validated AppleLoginRequestDto appleReqDto) { + if (appleReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleLoginVersion2(appleReqDto)); + } + + @PostMapping("/v2/apple/signup") + public ApiResponse appleSignUpVersion2( + @RequestBody @Validated AppleSignUpRequestVersion2Dto appleSignUpReqDto) { + if (appleSignUpReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleSignUpVersion2(appleSignUpReqDto)); + } + + // 카카오 탈퇴 + @DeleteMapping("/withdraw/kakao") + public ApiResponse kakaoWithdraw(@AuthenticationPrincipal Member member, + @RequestHeader("target-id") String kakaoTargetId, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { + Long targetId; + + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } else { + targetId = Long.parseLong(kakaoTargetId); + if (!targetId.equals(member.getKakaoTargetId())) { + throw new ExceptionHandler(UNCORRECTED_TARGET_ID); + } + } + + // 카카오 계정 연결 끊기 + // TODO : 해당 로직을 스케쥴러로 이동할 필요가 있음. + boolean isUnlink = oauthService.kakaoWithdraw(member, targetId); + + // 탈퇴 사유 추가 + if (withdrawReasonReqDto.getWithdrawReason() != null) { + withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); + } + + // 사용자 정보 삭제 (DB) + if (!isUnlink) { + throw new ExceptionHandler(NOT_UNLINK_KAKAO); + } + + return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); + } + + // 애플 탈퇴 + @DeleteMapping("/withdraw/apple") + public ApiResponse withdraw(@AuthenticationPrincipal Member member, + @Nullable @RequestHeader("X-Apple-Code") final String code, + @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { + oauthService.appleWithdraw(member, code); + + // 탈퇴 사유 추가 + if (withdrawReasonReqDto.getWithdrawReason() != null) { + withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); + } + + return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java b/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java new file mode 100644 index 00000000..24d0110f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java @@ -0,0 +1,338 @@ +package umc.th.juinjang.api.auth.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.auth.controller.request.AppleInfo; +import umc.th.juinjang.api.auth.controller.request.AppleLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.controller.request.KakaoLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.service.response.LoginResponseDto; +import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; +import umc.th.juinjang.auth.jwt.JwtService; +import umc.th.juinjang.auth.jwt.TokenDto; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; +import umc.th.juinjang.domain.member.model.MemberStatus; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.event.publisher.MemberEventPublisher; +import umc.th.juinjang.external.openfeign.apple.AppleClientSecretGenerator; +import umc.th.juinjang.external.openfeign.apple.AppleOAuthProvider; +import umc.th.juinjang.external.openfeign.kakao.KakaoUnlinkClient; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OAuthServiceV2 { + + private final MemberRepository memberRepository; + private final JwtService jwtService; + private final AppleClientSecretGenerator appleClientSecretGenerator; + private final AppleOAuthProvider appleOAuthProvider; + private final MemberEventPublisher memberEventPublisher; + + @Autowired + private KakaoUnlinkClient kakaoUnlinkClient; + + @Value("${security.oauth2.client.registration.kakao.admin-key}") + private String kakaoAdminKey; + + @Transactional + public LoginResponseDto kakaoLogin(Long targetId, KakaoLoginRequestDto kakaoLoginRequestDto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + kakaoLoginRequestDto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + return member.map(this::createToken) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO)); + } + + @Transactional + public LoginResponseDto kakaoSignUp(Long targetId, KakaoSignUpRequestDto kakaoLoginRequestDto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + kakaoLoginRequestDto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createKakaoMember( + kakaoLoginRequestDto.getEmail(), + targetId, + kakaoLoginRequestDto.getNickname(), + null + ) + ); + + publishDiscordAlert(newMember); + return createToken(newMember); + } + } + + @Transactional + public LoginResponseVersion2Dto kakaoLoginVersion2(Long targetId, KakaoLoginRequestDto dto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + dto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + return member.map(this::createTokenVersion2) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO)); + } + + @Transactional + public LoginResponseVersion2Dto kakaoSignUpVersion2(Long targetId, KakaoSignUpRequestVersion2Dto dto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + dto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createKakaoMember( + dto.getEmail(), + targetId, + dto.getNickname(), + null + ) + ); + + publishDiscordAlert(newMember); + return createTokenVersion2(newMember); + } + } + + @Transactional + public String logout(String refreshToken) { + Optional getMember = memberRepository.findByRefreshToken(refreshToken); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (member.getRefreshToken().equals("")) + throw new MemberHandler(ALREADY_LOGOUT); + + member.refreshTokenExpires(); + memberRepository.save(member); + + return "로그아웃 성공"; + } + + @Transactional + public LoginResponseDto regenerateAccessToken(String accessToken, String refreshToken) { + if (jwtService.validateTokenBoolean(accessToken)) // access token 유효성 검사 + throw new ExceptionHandler(ACCESS_TOKEN_AUTHORIZED); + + if (!jwtService.validateTokenBoolean(refreshToken)) // refresh token 유효성 검사 + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + Long memberId = jwtService.getMemberIdFromJwtToken(refreshToken); + + Optional getMember = memberRepository.findById(memberId); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (!refreshToken.equals(member.getRefreshToken())) + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + String newRefreshToken = jwtService.encodeJwtRefreshToken(memberId); + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(memberId)); + + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getNickname()); + } + + @Transactional + public LoginResponseDto createToken(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getEmail()); + } + + @Transactional + public LoginResponseVersion2Dto createTokenVersion2(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + + return new LoginResponseVersion2Dto(newAccessToken, newRefreshToken, member.getEmail(), + member.getAgreeVersion()); + } + + private void publishDiscordAlert(Member member) { + memberEventPublisher.publishSignUpEvent(member); + } + + @Transactional + public LoginResponseDto appleLogin(AppleLoginRequestDto request) { + log.info("Oauth service 까지 들어옴{}", request.getIdentityToken()); + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + return member.map(this::createToken) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO)); + } + + @Transactional + public LoginResponseDto appleSignUp(AppleSignUpRequestDto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createAppleMember( + email, + sub, + request.getNickname(), + null + ) + ); + + publishDiscordAlert(newMember); + return createToken(newMember); + } + } + + @Transactional + public LoginResponseVersion2Dto appleLoginVersion2(AppleLoginRequestDto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + return member.map(this::createTokenVersion2) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO)); + } + + @Transactional + public LoginResponseVersion2Dto appleSignUpVersion2(AppleSignUpRequestVersion2Dto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createAppleMember( + email, + sub, + request.getNickname(), + request.getAgreeVersion() + ) + ); + + publishDiscordAlert(newMember); + return createTokenVersion2(newMember); + } + } + + @Transactional + public boolean kakaoWithdraw(Member member, Long targetId) { + ResponseEntity response = kakaoUnlinkClient.unlinkUser("KakaoAK " + kakaoAdminKey, "user_id", + targetId); + + if (response.getStatusCode().is2xxSuccessful()) { // 성공 처리 로직 + log.info("카카오 탈퇴 성공"); + log.info("member id :: {}", member.getMemberId()); + + member.kakaoWithdraw(); + + return true; + } else { // 실패 처리 로직 + return false; + } + } + + @Transactional + public void appleWithdraw(Member member, String code) { + if (member.getProvider() != MemberProvider.APPLE) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } + try { + String clientSecret = appleClientSecretGenerator.generateClientSecret(); + String refreshToken = appleOAuthProvider.getAppleRefreshToken(code, clientSecret); + appleOAuthProvider.requestRevoke(refreshToken, clientSecret); + } catch (Exception e) { + throw new MemberHandler(FAILED_TO_LOAD_PRIVATE_KEY); + } + log.info("애플 탈퇴 성공"); + log.info("member id :: {}", member.getMemberId()); + + member.appleWithdraw(); + } +} diff --git a/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java b/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java index 76f0a325..b0d5cafe 100644 --- a/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java @@ -1,32 +1,34 @@ package umc.th.juinjang.auth.jwt; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import umc.th.juinjang.common.ExceptionHandler; import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.domain.member.model.MemberStatus; import umc.th.juinjang.domain.member.repository.MemberRepository; @Slf4j @RequiredArgsConstructor @Service public class UserDetailServiceImpl implements UserDetailsService { - private final MemberRepository memberRepository; - + private final MemberRepository memberRepository; - public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { - System.out.println("로그인한 memberId : " + memberId); - UserDetails result = (UserDetails) memberRepository.findById(Long.parseLong(memberId)) - .orElseThrow(() -> new ExceptionHandler(ErrorStatus.MEMBER_NOT_FOUND)); - log.info("UserDetails: 여기ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ"); - //로그인할 때 result.getUsername() 여기서 에러남 -// log.info("UserDetails: " + result.getUsername()); - log.info("UserDetails: " + result.toString()); + public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { + System.out.println("로그인한 memberId : " + memberId); + UserDetails result = (UserDetails)memberRepository.findByMemberIdAndStatus( + Long.parseLong(memberId), + MemberStatus.ACTIVE + ).orElseThrow(() -> new ExceptionHandler(ErrorStatus.MEMBER_NOT_FOUND)); + log.info("UserDetails: 여기ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ"); + //로그인할 때 result.getUsername() 여기서 에러남 + // log.info("UserDetails: " + result.getUsername()); + log.info("UserDetails: " + result.toString()); - return result; - } + return result; + } } diff --git a/src/main/java/umc/th/juinjang/domain/member/model/Member.java b/src/main/java/umc/th/juinjang/domain/member/model/Member.java index 6ee33a02..bc473e35 100644 --- a/src/main/java/umc/th/juinjang/domain/member/model/Member.java +++ b/src/main/java/umc/th/juinjang/domain/member/model/Member.java @@ -56,11 +56,11 @@ public class Member extends BaseEntity implements UserDetails { private String agreeVersion; // apple client id값을 의미 - @Column(name = "apple_sub", unique = true) + @Column(name = "apple_sub") private String appleSub; // kakao target id값 의미 (카카오의 유저 식별값. 탈퇴할 때 필요) - @Column(name = "target_id", unique = true) + @Column(name = "target_id") private Long kakaoTargetId; @Lob @@ -74,7 +74,10 @@ public class Member extends BaseEntity implements UserDetails { private String introduction; - private String status; // TODO : 추후에 ENUM 으로 변경 필요 + @Enumerated(EnumType.STRING) + private MemberStatus status; + + private LocalDateTime deletedAt; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List pencilAccounts = new ArrayList<>(); @@ -94,6 +97,49 @@ public class Member extends BaseEntity implements UserDetails { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List likedNotes = new ArrayList<>(); + public static Member createKakaoMember(String email, Long targetId, String nickname, String agreeVersion) { + String introduction = String.format("안녕하세요, %s 입니다.", nickname); + + Member member = Member.builder() + .email(email) + .provider(MemberProvider.KAKAO) + .kakaoTargetId(targetId) + .nickname(nickname) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now()) + .agreeVersion(agreeVersion) + .introduction(introduction) + .status(MemberStatus.ACTIVE) + .build(); + + PencilAccount createAccount = PencilAccount.createPencilAccount(member); + member.addPencilAccount(createAccount); + + return member; + } + + // 애플 회원 생성 팩토리 메서드 + public static Member createAppleMember(String email, String sub, String nickname, String agreeVersion) { + String introduction = String.format("안녕하세요, %s 입니다.", nickname); + + Member member = Member.builder() + .email(email) + .nickname(nickname) + .provider(MemberProvider.APPLE) + .appleSub(sub) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now()) + .agreeVersion(agreeVersion) + .introduction(introduction) + .status(MemberStatus.ACTIVE) + .build(); + + PencilAccount createAccount = PencilAccount.createPencilAccount(member); + member.addPencilAccount(createAccount); + + return member; + } + // refreshToken 재발급 public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; @@ -153,41 +199,6 @@ public void updateAgreeVersion(final String agreeVersion) { this.agreeVersion = agreeVersion; } - public static Member createKakaoMember(String email, Long targetId, String nickname, String agreeVersion) { - Member member = Member.builder() - .email(email) - .provider(MemberProvider.KAKAO) - .kakaoTargetId(targetId) - .nickname(nickname) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .agreeVersion(agreeVersion) - .build(); - - PencilAccount createAccount = PencilAccount.createPencilAccount(member); - member.addPencilAccount(createAccount); - - return member; - } - - // 애플 회원 생성 팩토리 메서드 - public static Member createAppleMember(String email, String sub, String nickname, String agreeVersion) { - Member member = Member.builder() - .email(email) - .nickname(nickname) - .provider(MemberProvider.APPLE) - .appleSub(sub) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .agreeVersion(agreeVersion) - .build(); - - PencilAccount createAccount = PencilAccount.createPencilAccount(member); - member.addPencilAccount(createAccount); - - return member; - } - public PencilAccount getAccount() { if (this.pencilAccounts == null || this.pencilAccounts.isEmpty()) { return null; @@ -202,4 +213,18 @@ public void addPencilAccount(PencilAccount pencilAccount) { } this.pencilAccounts.add(pencilAccount); } + + public void kakaoWithdraw() { + this.status = MemberStatus.WITHDRAWN; + this.kakaoTargetId = null; + this.nickname = null; + this.deletedAt = LocalDateTime.now(); + } + + public void appleWithdraw() { + this.status = MemberStatus.WITHDRAWN; + this.appleSub = null; + this.nickname = null; + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java b/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java new file mode 100644 index 00000000..69d937e7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java @@ -0,0 +1,6 @@ +package umc.th.juinjang.domain.member.model; + +public enum MemberStatus { + ACTIVE, + WITHDRAWN +} diff --git a/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java b/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java index ac3eaacf..f066cc52 100644 --- a/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java +++ b/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.repository.query.Param; import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberStatus; public interface MemberRepository extends JpaRepository { @@ -26,4 +27,10 @@ public interface MemberRepository extends JpaRepository { void patchIntroduction(@Param("id") Long id, @Param("introduction") String introduction); boolean existsByNickname(String nickname); + + Optional findByEmailAndKakaoTargetIdAndStatus(String email, Long kakaoTargetId, MemberStatus status); + + Optional findByEmailAndAppleSubAndStatus(String email, String sub, MemberStatus status); + + Optional findByMemberIdAndStatus(long id, MemberStatus memberStatus); }