diff --git a/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java b/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java new file mode 100644 index 0000000..4350456 --- /dev/null +++ b/doorip-api/src/main/java/org/doorip/user/api/UserApiController.java @@ -0,0 +1,26 @@ +package org.doorip.user.api; + +import lombok.RequiredArgsConstructor; +import org.doorip.common.ApiResponse; +import org.doorip.common.ApiResponseUtil; +import org.doorip.message.SuccessMessage; +import org.doorip.user.dto.request.UserSignInRequest; +import org.doorip.user.dto.response.UserResponse; +import org.doorip.user.service.UserService; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/api/users") +@Controller +public class UserApiController { + private final UserService userService; + + @PostMapping("/signin") + public ResponseEntity> signIn(@RequestHeader("Authorization") final String token, + @RequestParam final UserSignInRequest request) { + final UserResponse response = userService.signIn(token, request); + return ApiResponseUtil.success(SuccessMessage.OK, response); + } +} diff --git a/doorip-api/src/main/java/org/doorip/user/dto/request/UserSignInRequest.java b/doorip-api/src/main/java/org/doorip/user/dto/request/UserSignInRequest.java new file mode 100644 index 0000000..8a36def --- /dev/null +++ b/doorip-api/src/main/java/org/doorip/user/dto/request/UserSignInRequest.java @@ -0,0 +1,6 @@ +package org.doorip.user.dto.request; + +public record UserSignInRequest( + String platform +) { +} diff --git a/doorip-api/src/main/java/org/doorip/user/dto/response/UserResponse.java b/doorip-api/src/main/java/org/doorip/user/dto/response/UserResponse.java new file mode 100644 index 0000000..9677b69 --- /dev/null +++ b/doorip-api/src/main/java/org/doorip/user/dto/response/UserResponse.java @@ -0,0 +1,16 @@ +package org.doorip.user.dto.response; + +import org.doorip.auth.jwt.Token; + +public record UserResponse( + String accessToken, + String refreshToken +) { + + public static UserResponse of(Token token) { + return new UserResponse( + token.accessToken(), + token.refreshToken() + ); + } +} diff --git a/doorip-api/src/main/java/org/doorip/user/service/UserService.java b/doorip-api/src/main/java/org/doorip/user/service/UserService.java new file mode 100644 index 0000000..1c56712 --- /dev/null +++ b/doorip-api/src/main/java/org/doorip/user/service/UserService.java @@ -0,0 +1,60 @@ +package org.doorip.user.service; + +import lombok.RequiredArgsConstructor; +import org.doorip.auth.jwt.JwtProvider; +import org.doorip.auth.jwt.Token; +import org.doorip.exception.EntityNotFoundException; +import org.doorip.message.ErrorMessage; +import org.doorip.openfeign.apple.AppleOAuthProvider; +import org.doorip.openfeign.kakao.KakaoOAuthProvider; +import org.doorip.user.domain.Platform; +import org.doorip.user.domain.RefreshToken; +import org.doorip.user.domain.User; +import org.doorip.user.dto.request.UserSignInRequest; +import org.doorip.user.dto.response.UserResponse; +import org.doorip.user.repository.RefreshTokenRepository; +import org.doorip.user.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static org.doorip.user.domain.Platform.APPLE; +import static org.doorip.user.domain.Platform.getEnumPlatformFromStringPlatform; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class UserService { + private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final JwtProvider jwtProvider; + private final AppleOAuthProvider appleOAuthProvider; + private final KakaoOAuthProvider kakaoOAuthProvider; + + @Transactional + public UserResponse signIn(String token, UserSignInRequest request) { + Platform enumPlatform = getEnumPlatformFromStringPlatform(request.platform()); + String platformId = getPlatformId(token, enumPlatform); + User findUser = getUser(enumPlatform, platformId); + Token issueToken = jwtProvider.issueToken(findUser.getId()); + updateRefreshToken(issueToken.refreshToken(), findUser); + + return UserResponse.of(issueToken); + } + + private String getPlatformId(String token, Platform platform) { + if (platform == APPLE) { + return appleOAuthProvider.getApplePlatformId(token); + } + return kakaoOAuthProvider.getKakaoPlatformId(token); + } + + private User getUser(Platform platform, String platformId) { + return userRepository.findUserByPlatformAndPlatformId(platform, platformId) + .orElseThrow(() -> new EntityNotFoundException(ErrorMessage.USER_NOT_FOUND)); + } + + private void updateRefreshToken(String refreshToken, User user) { + user.updateRefreshToken(refreshToken); + refreshTokenRepository.save(RefreshToken.createRefreshToken(user.getId(), refreshToken)); + } +} diff --git a/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java b/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java index 8de3cef..9391bdb 100644 --- a/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java +++ b/doorip-common/src/main/java/org/doorip/message/ErrorMessage.java @@ -12,6 +12,7 @@ public enum ErrorMessage { * 400 Bad Request */ BAD_REQUEST(HttpStatus.BAD_REQUEST, "e4000", "잘못된 요청입니다."), + INVALID_PLATFORM_TYPE(HttpStatus.BAD_REQUEST, "e4001", "유효하지 않은 플랫폼 타입입니다."), /** * 401 Unauthorized @@ -40,6 +41,7 @@ public enum ErrorMessage { * 404 Not Found */ ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "e4040", "대상을 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "e4041", "존재하지 않는 회원입니다."), /** * 405 Method Not Allowed diff --git a/doorip-domain/build.gradle b/doorip-domain/build.gradle index 93144c9..f73e9f0 100644 --- a/doorip-domain/build.gradle +++ b/doorip-domain/build.gradle @@ -1,5 +1,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' implementation project(path: ':doorip-common') diff --git a/doorip-domain/src/main/java/org/doorip/user/domain/Platform.java b/doorip-domain/src/main/java/org/doorip/user/domain/Platform.java index c46c049..d3a7fd6 100644 --- a/doorip-domain/src/main/java/org/doorip/user/domain/Platform.java +++ b/doorip-domain/src/main/java/org/doorip/user/domain/Platform.java @@ -1,5 +1,23 @@ package org.doorip.user.domain; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.doorip.exception.InvalidValueException; +import org.doorip.message.ErrorMessage; + +import java.util.Arrays; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum Platform { - KAKAO, APPLE -} + APPLE("apple"), + KAKAO("kakao"); + + private final String stringPlatform; + + public static Platform getEnumPlatformFromStringPlatform(String stringPlatform) { + return Arrays.stream(values()) + .filter(platform -> platform.stringPlatform.equals(stringPlatform)) + .findFirst() + .orElseThrow(() -> new InvalidValueException(ErrorMessage.INVALID_PLATFORM_TYPE)); + } +} \ No newline at end of file diff --git a/doorip-domain/src/main/java/org/doorip/user/domain/RefreshToken.java b/doorip-domain/src/main/java/org/doorip/user/domain/RefreshToken.java new file mode 100644 index 0000000..0c78345 --- /dev/null +++ b/doorip-domain/src/main/java/org/doorip/user/domain/RefreshToken.java @@ -0,0 +1,25 @@ +package org.doorip.user.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +@Getter +@RedisHash(value = "refreshToken", timeToLive = 604800000) +public class RefreshToken { + @Id + private Long id; + private String refreshToken; + + public static RefreshToken createRefreshToken(Long userId, String refreshToken) { + return RefreshToken.builder() + .id(userId) + .refreshToken(refreshToken) + .build(); + } +} \ No newline at end of file diff --git a/doorip-domain/src/main/java/org/doorip/user/domain/User.java b/doorip-domain/src/main/java/org/doorip/user/domain/User.java index abd5ea8..7f994e2 100644 --- a/doorip-domain/src/main/java/org/doorip/user/domain/User.java +++ b/doorip-domain/src/main/java/org/doorip/user/domain/User.java @@ -33,4 +33,8 @@ public class User extends BaseTimeEntity { @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private List participants = new ArrayList<>(); + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } diff --git a/doorip-domain/src/main/java/org/doorip/user/repository/RefreshTokenRepository.java b/doorip-domain/src/main/java/org/doorip/user/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..ce9e23f --- /dev/null +++ b/doorip-domain/src/main/java/org/doorip/user/repository/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package org.doorip.user.repository; + +import org.doorip.user.domain.RefreshToken; +import org.springframework.data.repository.CrudRepository; + +public interface RefreshTokenRepository extends CrudRepository { +} diff --git a/doorip-domain/src/main/java/org/doorip/user/repository/UserRepository.java b/doorip-domain/src/main/java/org/doorip/user/repository/UserRepository.java new file mode 100644 index 0000000..539531e --- /dev/null +++ b/doorip-domain/src/main/java/org/doorip/user/repository/UserRepository.java @@ -0,0 +1,11 @@ +package org.doorip.user.repository; + +import org.doorip.user.domain.Platform; +import org.doorip.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findUserByPlatformAndPlatformId(Platform platform, String platformId); +}