Skip to content

Commit

Permalink
Merge pull request #21 from Team-HMH/feat/#18-social-sign-up-api
Browse files Browse the repository at this point in the history
feat - νšŒμ›κ°€μž… κΈ°λŠ₯ κ΅¬ν˜„
  • Loading branch information
kseysh authored Jan 10, 2024
2 parents 8987ae2 + 700ea6d commit 080e8b7
Show file tree
Hide file tree
Showing 21 changed files with 223 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package sopt.org.HMH.domain.challenge.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import sopt.org.HMH.domain.app.dto.request.AppGoalTimeRequest;

import java.util.List;

public record ChallengeRequest(

Integer period,
Long goalTime,
List<AppGoalTimeRequest> apps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sopt.org.HMH.domain.user.domain.exception.UserSuccess;
import sopt.org.HMH.domain.user.dto.request.SocialLoginRequest;
import sopt.org.HMH.domain.user.dto.request.SocialPlatformRequest;
import sopt.org.HMH.domain.user.dto.request.SocialSignUpRequest;
import sopt.org.HMH.domain.user.dto.response.LoginResponse;
import sopt.org.HMH.domain.user.service.UserService;
import sopt.org.HMH.global.auth.jwt.JwtProvider;
Expand All @@ -28,13 +29,23 @@ public class UserController {
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(
@RequestHeader("Authorization") String socialAccessToken,
@RequestBody SocialLoginRequest request
@RequestBody SocialPlatformRequest request
) {
return ResponseEntity
.status(UserSuccess.LOGIN_SUCCESS.getHttpStatus())
.body(ApiResponse.success(UserSuccess.LOGIN_SUCCESS, userService.login(socialAccessToken, request)));
}

@PostMapping("/signup")
public ResponseEntity<ApiResponse<LoginResponse>> signup(
@RequestHeader("Authorization") String socialAccessToken,
@RequestBody SocialSignUpRequest request
) {
return ResponseEntity
.status(UserSuccess.SIGNUP_SUCCESS.getHttpStatus())
.body(ApiResponse.success(UserSuccess.SIGNUP_SUCCESS, userService.signup(socialAccessToken, request)));
}

@GetMapping("/reissue")
public ResponseEntity<ApiResponse<TokenDto>> reissue(
@RequestHeader("Authorization") String refreshToken
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/sopt/org/HMH/domain/user/domain/OnboardingInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sopt.org.HMH.domain.user.domain;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "onboarding_info")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OnboardingInfo {

@Id
@Column(name = "onboarding_info_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "onboarding_info_id")
private List<OnboardingProblem> problem;

private String averageUseTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package sopt.org.HMH.domain.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "problem")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class OnboardingProblem {

@Id
@Column(name = "problem_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String problem;
}
12 changes: 12 additions & 0 deletions src/main/java/sopt/org/HMH/domain/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sopt.org.HMH.domain.challenge.domain.Challenge;
import sopt.org.HMH.global.auth.social.SocialPlatform;
import sopt.org.HMH.global.common.domain.BaseTimeEntity;

Expand Down Expand Up @@ -42,6 +47,13 @@ public class User extends BaseTimeEntity {
@Column(name = "profile_image_url")
private String profileImageUrl;

@OneToOne
@JoinColumn(name = "onboarding_info_id")
private OnboardingInfo onboardingInfo;

@OneToMany(mappedBy = "user")
private List<Challenge> challenges;

public void updateSocialInfo(String nickname, String profileImageUrl) {
this.name = nickname;
this.profileImageUrl = profileImageUrl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum UserError implements ErrorBase {

// 400 BAD REQUEST
INVALID_USER(HttpStatus.BAD_REQUEST, "Principle 객체가 μ—†μŠ΅λ‹ˆλ‹€."),
DUPLICATE_USER(HttpStatus.BAD_REQUEST, "이미 νšŒμ›κ°€μž…λœ μœ μ €μž…λ‹ˆλ‹€."),

// 401 UNAUTHORIZED

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
public enum UserSuccess implements SuccessBase {

LOGIN_SUCCESS(HttpStatus.OK, "λ‘œκ·ΈμΈμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
SIGNUP_SUCCESS(HttpStatus.OK, "νšŒμ›κ°€μž…μ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
REISSUE_SUCCESS(HttpStatus.OK, "토큰 μž¬λ°œκΈ‰μ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
LOGOUT_SUCCESS(HttpStatus.OK, "λ‘œκ·Έμ•„μ›ƒμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€."),
;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sopt.org.HMH.domain.user.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

public record OnboardingRequest(
String averageUseTime,

@JsonProperty(value = "problem")
List<String> problemList
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sopt.org.HMH.global.auth.social.SocialPlatform;

public record SocialLoginRequest(
public record SocialPlatformRequest(
SocialPlatform socialPlatform
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package sopt.org.HMH.domain.user.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import sopt.org.HMH.domain.challenge.dto.request.ChallengeRequest;
import sopt.org.HMH.global.auth.social.SocialPlatform;

public record SocialSignUpRequest(
SocialPlatform socialPlatform,

@JsonProperty(value = "onboarding")
OnboardingRequest onboardingRequest,

@JsonProperty(value = "challenge")
ChallengeRequest challengeRequest
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sopt.org.HMH.domain.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import sopt.org.HMH.domain.user.domain.OnboardingInfo;

public interface OnboardingInfoRepository extends JpaRepository<OnboardingInfo, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ default User findByIdOrThrowException(Long userId) {
}

Optional<User> findBySocialPlatformAndSocialId(SocialPlatform socialPlatform, Long socialId);
boolean existsBySocialPlatformAndSocialId(SocialPlatform socialPlatform, Long socialId);
}
98 changes: 80 additions & 18 deletions src/main/java/sopt/org/HMH/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package sopt.org.HMH.domain.user.service;

import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sopt.org.HMH.domain.user.domain.OnboardingInfo;
import sopt.org.HMH.domain.user.domain.OnboardingProblem;
import sopt.org.HMH.domain.user.domain.User;
import sopt.org.HMH.domain.user.domain.exception.UserError;
import sopt.org.HMH.domain.user.domain.exception.UserException;
import sopt.org.HMH.domain.user.dto.request.SocialLoginRequest;
import sopt.org.HMH.domain.user.dto.request.SocialPlatformRequest;
import sopt.org.HMH.domain.user.dto.request.SocialSignUpRequest;
import sopt.org.HMH.domain.user.dto.response.LoginResponse;
import sopt.org.HMH.domain.user.repository.OnboardingInfoRepository;
import sopt.org.HMH.domain.user.repository.UserRepository;
import sopt.org.HMH.global.auth.jwt.JwtProvider;
import sopt.org.HMH.global.auth.jwt.TokenDto;
Expand All @@ -24,28 +30,38 @@ public class UserService {

private final JwtProvider jwtProvider;
private final UserRepository userRepository;
private final OnboardingInfoRepository onboardingInfoRepository;
private final KakaoLoginService kakaoLoginService;

@Transactional
public LoginResponse login(String socialAccessToken, SocialLoginRequest request) {
socialAccessToken = parseTokenString(socialAccessToken);
public LoginResponse login(String socialAccessToken, SocialPlatformRequest request) {

SocialPlatform socialPlatform = request.socialPlatform();
Long socialId = getUserIdBySocialAccessToken(socialPlatform, socialAccessToken);
Long socialId = getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken);

// μœ μ €λ₯Ό 찾지 λͺ»ν•˜λ©΄ 404 Errorλ₯Ό 던져 ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ νšŒμ›κ°€μž… apiλ₯Ό μš”κ΅¬ν•œλ‹€.
User loginUser = getUserBySocialAndSocialId(socialPlatform, socialId);
User loginUser = getUserBySocialPlatformAndSocialId(socialPlatform, socialId);

if (socialPlatform == SocialPlatform.KAKAO) {
kakaoLoginService.updateUserInfoByKakao(loginUser, socialAccessToken);
}
return performLogin(socialAccessToken, socialPlatform, loginUser);
}

TokenDto tokenDto = jwtProvider.issueToken(new UserAuthentication(loginUser.getId(), null, null));
@Transactional
public LoginResponse signup(String socialAccessToken, SocialSignUpRequest request) {

return LoginResponse.of(loginUser, tokenDto);
SocialPlatform socialPlatform = request.socialPlatform();
Long socialId = getSocialIdBySocialAccessToken(socialPlatform, socialAccessToken);

// 이미 νšŒμ›κ°€μž…λœ μœ μ €κ°€ μžˆλ‹€λ©΄ 400 Error λ°œμƒ
validateDuplicateUser(socialId, socialPlatform);

OnboardingInfo onboardingInfo = createOnboardingInfo(request);
User user = createUser(socialPlatform, socialId, onboardingInfo);

return performLogin(socialAccessToken, socialPlatform, user);
}

@Transactional
public TokenDto reissueToken(String refreshToken) {

refreshToken = parseTokenString(refreshToken);
Long userId = jwtProvider.validateRefreshToken(refreshToken);
validateUserId(userId); // userIdκ°€ DB에 μ €μž₯된 μœ νš¨ν•œ 값인지 검사
Expand All @@ -64,23 +80,69 @@ private void validateUserId(Long userId) {
}
}

private User getUserBySocialAndSocialId(SocialPlatform socialPlatform, Long socialId) {
private User getUserBySocialPlatformAndSocialId(SocialPlatform socialPlatform, Long socialId) {
return userRepository.findBySocialPlatformAndSocialIdOrThrowException(socialPlatform, socialId);
}

private Long getUserIdBySocialAccessToken(SocialPlatform socialPlatform, String socialAccessToken) {
private Long getSocialIdBySocialAccessToken(SocialPlatform socialPlatform, String socialAccessToken) {
return switch (socialPlatform.toString()) {
case "KAKAO" -> kakaoLoginService.getUserIdByKakao(socialAccessToken);
case "KAKAO" -> kakaoLoginService.getSocialIdByKakao(socialAccessToken);
default -> throw new JwtException(JwtError.INVALID_SOCIAL_ACCESS_TOKEN);
};
}

private static String parseTokenString(String tokenString) {
String[] strings = tokenString.split(" ");
if (strings.length != 2) {
/**
* μ†Œμ…œ μ•‘μ„ΈμŠ€ ν† ν°μ—μ„œ "Bearer " 뢀뢄을 μ‚­μ œμ‹œν‚€κ³  μœ νš¨ν•œ μ†Œμ…œ μ•‘μ„ΈμŠ€ ν† ν°λ§Œμ„ λ°›κΈ° μœ„ν•œ ν•¨μˆ˜
*/
private String parseTokenString(String tokenString) {
String[] parsedTokens = tokenString.split(" ");
if (parsedTokens.length != 2) {
throw new JwtException(JwtError.INVALID_TOKEN_HEADER);
}
return strings[1];
String validSocialAccessToken = parsedTokens[1];
return validSocialAccessToken;
}

private void validateDuplicateUser(Long socialId, SocialPlatform socialPlatform) {
if (userRepository.existsBySocialPlatformAndSocialId(socialPlatform, socialId)) {
throw new UserException(UserError.DUPLICATE_USER);
}
}

private LoginResponse performLogin(String socialAccessToken, SocialPlatform socialPlatform, User loginUser) {
if (socialPlatform == SocialPlatform.KAKAO) {
kakaoLoginService.updateUserInfoByKakao(loginUser, socialAccessToken);
}
TokenDto tokenDto = jwtProvider.issueToken(new UserAuthentication(loginUser.getId(), null, null));
return LoginResponse.of(loginUser, tokenDto);
}

private User createUser(SocialPlatform socialPlatform, Long socialId, OnboardingInfo onboardingInfo) {
User user = User.builder()
.socialPlatform(socialPlatform)
.socialId(socialId)
.onboardingInfo(onboardingInfo)
.build();
userRepository.save(user);
return user;
}

private OnboardingInfo createOnboardingInfo(SocialSignUpRequest request) {
List<OnboardingProblem> problemList = new ArrayList<>();
for (String problem : request.onboardingRequest().problemList()) {
problemList.add(
OnboardingProblem.builder()
.problem(problem)
.build()
);
}

OnboardingInfo onboardingInfo = OnboardingInfo.builder()
.averageUseTime(request.onboardingRequest().averageUseTime())
.problem(problemList)
.build();
onboardingInfoRepository.save(onboardingInfo);
return onboardingInfo;
}

public User getUserId(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import sopt.org.HMH.global.auth.social.kakao.response.KakaoUserResponse;
import sopt.org.HMH.global.auth.social.kakao.request.KakaoUserRequest;

@FeignClient(name = "kakaoApiClient", url = "${oauth2.kakao.base-url}")
public interface KakaoApiClient {

@GetMapping(value = "/v2/user/me")
KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);
KakaoUserRequest getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);
}
Loading

0 comments on commit 080e8b7

Please sign in to comment.