Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
HELP.md
.gradle
.env
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.7'
}

Expand Down Expand Up @@ -49,6 +49,10 @@ dependencies {

//validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// feign

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3'
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/rootbox/rootboxApp/RootboxAppApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class RootboxAppApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import rootbox.rootboxApp.api.user.implementation.UserCommandAdapter;
import rootbox.rootboxApp.api.user.implementation.UserQueryAdapter;
import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto;
import rootbox.rootboxApp.global.entity.RefreshToken;
import rootbox.rootboxApp.global.entity.User;
import rootbox.rootboxApp.global.entity.enums.user.SocialType;
import rootbox.rootboxApp.global.entity.enums.user.UserRole;
import rootbox.rootboxApp.global.feign.dto.OAuthInfoDto;
import rootbox.rootboxApp.global.feign.service.KakaoOauthService;
import rootbox.rootboxApp.global.security.provider.TokenProvider;

import java.util.List;
import java.util.Optional;

@Service
Expand All @@ -15,8 +28,80 @@ public class UserService {

private final UserQueryAdapter userQueryAdapter;

private final UserCommandAdapter userCommandAdapter;

private final KakaoOauthService kakaoOauthService;

private final TokenProvider tokenProvider;


Optional<User> findById(String id) {
return userQueryAdapter.findUserByIdSecurity(id);
}

@Transactional
public SocialLoginDto.KakaoSocialLoginResponseDto socialLogin(SocialLoginDto.KakaoSocialLoginRequestDto request) {

String kakaoToken = request.getKakaoToken();
String requestToken = "Bearer " + kakaoToken;

OAuthInfoDto kakaoUserInfo = kakaoOauthService.getKakaoUserInfo(requestToken);

Optional<User> userBySocialId = userQueryAdapter.findUserBySocialId(kakaoUserInfo.getId());

// 로그인 처리
if (userBySocialId.isPresent()) {
Optional<RefreshToken> refreshTokenByUserId = userQueryAdapter.findRefreshTokenByUserId(kakaoUserInfo.getId());

String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name())));

// 리프레시 토큰이 존재할 때
if (refreshTokenByUserId.isPresent()) {
return SocialLoginDto.KakaoSocialLoginResponseDto.builder()
.accessToken(accessToken)
.isNew(false)
.loginType(SocialType.KAKAO.name())
.refreshToken(refreshTokenByUserId.get().getRefreshToken())
.build();
}else{
// 리프레시 토큰 없음 만약 만료된 리프레시 토큰이면 추후에 만료 로직 탈 것이라 존재 유무만 봄
return SocialLoginDto.KakaoSocialLoginResponseDto.builder()
.accessToken(accessToken)
.isNew(false)
.loginType(SocialType.KAKAO.name())
.refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(),
userBySocialId.get().getSocialLoginUid()).getRefreshToken())
.build();
}
}else {
// 신규 가입 + 로그인
User user = userCommandAdapter.createUser(kakaoUserInfo.getId(), generateUniqueNickname());

String accessToken = tokenProvider.createAccessToken(user, List.of(new SimpleGrantedAuthority(UserRole.USER.name())));
return SocialLoginDto.KakaoSocialLoginResponseDto
.builder()
.loginType(SocialType.KAKAO.name())
.isNew(true)
.accessToken(accessToken)
.refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(),
user.getSocialLoginUid()).getRefreshToken())
.build();
}
}

public String getKakaoCode(){
return kakaoOauthService.getKakaoCodeUrl();
}

public String getKakaoToken(String code){
return kakaoOauthService.getKakaoAccessToken(code);
}

private String generateUniqueNickname() {
String name = "";
do {
name = RandomStringUtils.random(8, true, true);
} while (userQueryAdapter.findUserByNickname(name).isPresent());
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package rootbox.rootboxApp.api.user.implementation;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import rootbox.rootboxApp.api.user.persistence.RefreshTokenRepository;
import rootbox.rootboxApp.api.user.persistence.UserRepository;
import rootbox.rootboxApp.global.annotations.Adapter;
import rootbox.rootboxApp.global.entity.RefreshToken;
import rootbox.rootboxApp.global.entity.User;
import rootbox.rootboxApp.global.entity.enums.user.UserRole;

@Adapter
@Slf4j
@RequiredArgsConstructor
public class UserCommandAdapter {

private final UserRepository userRepository;

private final RefreshTokenRepository refreshTokenRepository;

public User createUser(String username, String socialUid){

User newUser = User.builder()
.socialLoginUid(socialUid)
.nickname(username)
.userRole(UserRole.USER)
.build();

return userRepository.save(newUser);
}

public RefreshToken saveRefreshToken(String refreshToken, String userSocialId){

return refreshTokenRepository.save(
RefreshToken.builder()
.userSocialId(userSocialId)
.refreshToken(refreshToken)
.build()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package rootbox.rootboxApp.api.user.implementation;

import lombok.RequiredArgsConstructor;
import rootbox.rootboxApp.api.user.persistence.RefreshTokenRepository;
import rootbox.rootboxApp.api.user.persistence.UserRepository;
import rootbox.rootboxApp.global.annotations.Adapter;
import rootbox.rootboxApp.global.entity.RefreshToken;
import rootbox.rootboxApp.global.entity.User;

import java.util.Optional;
Expand All @@ -13,8 +15,21 @@ public class UserQueryAdapter {

private final UserRepository userRepository;

private final RefreshTokenRepository refreshTokenRepository;

public Optional<User> findUserByIdSecurity(String userId){
return userRepository.findById(Long.valueOf(userId));
}

public Optional<User> findUserByNickname(String nickname){
return userRepository.findByNickname(nickname);
}

public Optional<User> findUserBySocialId(String socialId){
return userRepository.findBySocialLoginUid(socialId);
}

public Optional<RefreshToken> findRefreshTokenByUserId(String userId){
return refreshTokenRepository.findByUserSocialId(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package rootbox.rootboxApp.api.user.persistence;

import org.springframework.data.jpa.repository.JpaRepository;
import rootbox.rootboxApp.global.entity.RefreshToken;

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Optional<RefreshToken> findByUserSocialId(String userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
public interface UserRepository extends JpaRepository<User, Long> {

public Optional<User> findById(Long id);

public Optional<User> findByNickname(String nickname);

public Optional<User> findBySocialLoginUid(String socialId);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
package rootbox.rootboxApp.api.user.presentation;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import rootbox.rootboxApp.api.user.business.UserService;
import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto;
import rootbox.rootboxApp.global.common.CommonResponse;

import java.io.IOException;

@RestController
@RequiredArgsConstructor
@Slf4j
@Validated
@Tag(name = "User Api", description = "rootbox 사용자 관련 Api입니다.")
@RequestMapping(value = "/api/v1/user")
public class UserApi {

@GetMapping(value = "/health")
private final UserService userService;

@GetMapping(value = "/api/v1/user/health")
public String health2() {return "I'm healthy!!!" ;}

@GetMapping(value = "/api/v1/users/auth/health")
public String health() {return "I'm healthy!!!" ;}

@PostMapping(value = "/api/v1/users/auth/kakao")
public CommonResponse<SocialLoginDto.KakaoSocialLoginResponseDto> kakaoSocialLogin(@RequestBody @Valid SocialLoginDto.KakaoSocialLoginRequestDto requestDto) {
return CommonResponse.onSuccess(userService.socialLogin(requestDto));
}
@GetMapping(value = "/api/v1/users/auth/kakao/code")
public void kakaoSocailLoginTest(HttpServletResponse response) throws IOException {
response.sendRedirect(userService.getKakaoCode());
}

@GetMapping(value = "/api/v1/users/auth/kakao/test")
public CommonResponse<String> getKakaoToken(@RequestParam("code") String code){
return CommonResponse.onSuccess(userService.getKakaoToken(code));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package rootbox.rootboxApp.api.user.presentation.dto;

import jakarta.validation.constraints.NotNull;
import lombok.*;


public class SocialLoginDto {

@Builder
@Getter
@Setter
public static class KakaoSocialLoginResponseDto {

@NotNull
String loginType;

@NotNull
Boolean isNew;

@NotNull
String accessToken;

@NotNull
String refreshToken;
}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class KakaoSocialLoginRequestDto {

@NotNull
String kakaoToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand All @@ -31,6 +33,27 @@
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

CommonResponse<Object> body = CommonResponse.onFailure(
GlobalErrorCode.BAD_BODY.getCode(),
"요청 본문을 읽을 수 없습니다. BODY 자체를 읽을 수 없는 상태입니다.. (형식 오류)",
null
);

return super.handleExceptionInternal(
ex,
body,
headers,
HttpStatus.BAD_REQUEST,
request
);
}


@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package rootbox.rootboxApp.global.common.exception.ThrowClass;

import rootbox.rootboxApp.global.common.exception.base.BaseErrorCode;
import rootbox.rootboxApp.global.common.exception.base.GeneralException;

public class CustomFeignClientException extends GeneralException {
public CustomFeignClientException(BaseErrorCode errorCode){
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
public enum GlobalErrorCode implements BaseErrorCode{


BAD_BODY(HttpStatus.BAD_REQUEST, "GLOBAL400", "요청 BODY 본문을 읽을 수 없습니다"),
// AUTH + 401 Unauthorized - 권한 없음
TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_1", "인증 토큰이 만료 되었습니다. 토큰을 재발급 해주세요"),
INVALID_TOKEN(UNAUTHORIZED, "AUTH401_2", "인증 토큰이 유효하지 않습니다."),
Expand Down
Loading