diff --git a/src/backend/auth_server/build.gradle b/src/backend/auth_server/build.gradle index 0f1b2a0c..7f054fd1 100644 --- a/src/backend/auth_server/build.gradle +++ b/src/backend/auth_server/build.gradle @@ -17,6 +17,12 @@ repositories { mavenCentral() } +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2" + } +} + dependencies { // Spring implementation 'org.springframework.boot:spring-boot-starter-web' @@ -24,8 +30,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' //db - //implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' - //implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Lombok compileOnly 'org.projectlombok:lombok' @@ -40,8 +46,8 @@ dependencies { //junit testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //External API - //implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0' + // Spring Cloud OpenFeign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' //Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java index 912431c9..76614250 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java @@ -1,9 +1,16 @@ package com.jootalkpia.auth_server; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing +@EnableFeignClients @SpringBootApplication +@ImportAutoConfiguration(FeignAutoConfiguration.class) public class AuthServerApplication { public static void main(String[] args) { diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserInfoResponse.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserInfoResponse.java new file mode 100644 index 00000000..3462f12d --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserInfoResponse.java @@ -0,0 +1,20 @@ +package com.jootalkpia.auth_server.client.dto; + + +import com.jootalkpia.auth_server.user.domain.SocialType; + +public record UserInfoResponse( + Long socialId, + SocialType socialType, + String email, + String socialNickname +) { + public static UserInfoResponse of( + final Long socialId, + final SocialType socialType, + final String email, + final String socialNickname + ) { + return new UserInfoResponse(socialId, socialType, email, socialNickname); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserLoginRequest.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserLoginRequest.java new file mode 100644 index 00000000..82c4e0f0 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/dto/UserLoginRequest.java @@ -0,0 +1,17 @@ +package com.jootalkpia.auth_server.client.dto; + +import com.jootalkpia.auth_server.user.domain.SocialType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record UserLoginRequest( + @NotNull(message = "소셜 로그인 종류가 입력되지 않았습니다.") + @Schema(description = "소셜로그인 타입", example = "KAKAO") + SocialType socialType, + + @NotBlank(message = "redirectUri가 입력되지 않았습니다.") + @Schema(description = "리다이텍트 uri 값", example = "http://localhost:5173/kakao/redirection") + String redirectUri +) { +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java new file mode 100644 index 00000000..b6228690 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java @@ -0,0 +1,15 @@ +package com.jootalkpia.auth_server.client.kakao; + + +import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") +public interface KakaoApiClient { + + @GetMapping(value = "/v2/user/me") + KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java new file mode 100644 index 00000000..1c420a9c --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java @@ -0,0 +1,19 @@ +package com.jootalkpia.auth_server.client.kakao; + + +import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") +public interface KakaoAuthApiClient { + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoAccessTokenResponse getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code + ); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java new file mode 100644 index 00000000..64ced85c --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java @@ -0,0 +1,77 @@ +package com.jootalkpia.auth_server.client.kakao; + +import com.jootalkpia.auth_server.client.dto.UserInfoResponse; +import com.jootalkpia.auth_server.client.dto.UserLoginRequest; +import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse; +import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse; +import com.jootalkpia.auth_server.client.service.SocialService; +import com.jootalkpia.auth_server.exception.CustomException; +import com.jootalkpia.auth_server.response.ErrorCode; +import com.jootalkpia.auth_server.user.domain.SocialType; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class KakaoSocialService implements SocialService { + + private static final String AUTH_CODE = "authorization_code"; + + @Value("${kakao.clientId}") + private String clientId; + private final KakaoApiClient kakaoApiClient; + private final KakaoAuthApiClient kakaoAuthApiClient; + + @Transactional + @Override + public UserInfoResponse login( + final String authorizationCode, + final UserLoginRequest loginRequest + ) { + String accessToken; + try { + // 인가 코드로 Access Token + Refresh Token 받아오기 + accessToken = getOAuth2Authentication(authorizationCode, loginRequest.redirectUri()); + } catch (FeignException e) { + throw new CustomException(ErrorCode.AUTHENTICATION_CODE_EXPIRED); + } + // Access Token으로 유저 정보 불러오기 + return getLoginDto(loginRequest.socialType(), getUserInfo(accessToken)); + } + + private String getOAuth2Authentication( + final String authorizationCode, + final String redirectUri + ) { + KakaoAccessTokenResponse response = kakaoAuthApiClient.getOAuth2AccessToken( + AUTH_CODE, + clientId, + redirectUri, + authorizationCode + ); + return response.accessToken(); + } + + private KakaoUserResponse getUserInfo( + final String accessToken + ) { + return kakaoApiClient.getUserInformation("Bearer " + accessToken); + } + + private UserInfoResponse getLoginDto( + final SocialType socialType, + final KakaoUserResponse userResponse + ) { + return UserInfoResponse.of( + userResponse.id(), + socialType, + userResponse.kakaoAccount().email(), + userResponse.kakaoAccount().profile().nickname() + ); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccessTokenResponse.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccessTokenResponse.java new file mode 100644 index 00000000..fb30bcfb --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccessTokenResponse.java @@ -0,0 +1,17 @@ +package com.jootalkpia.auth_server.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccessTokenResponse( + String accessToken +) { + public static KakaoAccessTokenResponse of( + final String accessToken + ) { + return new KakaoAccessTokenResponse( + accessToken + ); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccount.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccount.java new file mode 100644 index 00000000..4fa44e65 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoAccount.java @@ -0,0 +1,11 @@ +package com.jootalkpia.auth_server.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccount( + String email, + KakaoUserProfile profile +) { +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserProfile.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserProfile.java new file mode 100644 index 00000000..6eb85765 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserProfile.java @@ -0,0 +1,10 @@ +package com.jootalkpia.auth_server.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserProfile( + String nickname +) { +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserResponse.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserResponse.java new file mode 100644 index 00000000..cca74d23 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoUserResponse.java @@ -0,0 +1,11 @@ +package com.jootalkpia.auth_server.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserResponse( + Long id, + KakaoAccount kakaoAccount +) { +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java new file mode 100644 index 00000000..3eb12e5a --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java @@ -0,0 +1,9 @@ +package com.jootalkpia.auth_server.client.service; + + +import com.jootalkpia.auth_server.client.dto.UserInfoResponse; +import com.jootalkpia.auth_server.client.dto.UserLoginRequest; + +public interface SocialService { + UserInfoResponse login(final String authorizationToken, final UserLoginRequest loginRequest); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java index bcb4314c..16494a96 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java @@ -26,20 +26,17 @@ public class SecurityConfig { "/api/v1/user/login", "/api/v1/user/token-refresh", "/api/v1/actuator/health", - "/api/v1/v3/api-docs/**", - "/api/v1/swagger-ui/**", - "/api/v1/swagger-resources/**" + "/v3/**", + "/swagger-ui/**" }; - @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) - .sessionManagement(session -> { - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); - }) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exception -> { exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java new file mode 100644 index 00000000..ad6f5bb9 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java @@ -0,0 +1,12 @@ +package com.jootalkpia.auth_server.jwt; + +import com.jootalkpia.auth_server.redis.Token; +import java.util.Optional; +import org.springframework.data.repository.CrudRepository; + +public interface TokenRepository extends CrudRepository { + + Optional findByRefreshToken(final String refreshToken); + + Optional findById(final Long id); +} \ No newline at end of file diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java new file mode 100644 index 00000000..f2f0cbb9 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java @@ -0,0 +1,20 @@ +package com.jootalkpia.auth_server.jwt; + +import com.jootalkpia.auth_server.redis.Token; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + + @Transactional + public void saveRefreshToken(final Long userId, final String refreshToken) { + tokenRepository.save( + Token.of(userId, refreshToken) + ); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java index c3a89e31..947b75f1 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java @@ -12,6 +12,8 @@ public enum ErrorCode { BAD_REQUEST("A40001", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), MISSING_REQUIRED_HEADER("A40002", HttpStatus.BAD_REQUEST, "필수 헤더가 누락되었습니다."), MISSING_REQUIRED_PARAMETER("A40003", HttpStatus.BAD_REQUEST, "필수 파라미터가 누락되었습니다."), + AUTHENTICATION_CODE_EXPIRED("A40004", HttpStatus.BAD_REQUEST, "인가 코드가 만료되었습니다."), + SOCIAL_TYPE_BAD_REQUEST("A40005", HttpStatus.BAD_REQUEST, "로그인 요청이 유효하지 않습니다."), // 401 Unauthorized ACCESS_TOKEN_EXPIRED("A40100", HttpStatus.UNAUTHORIZED, "액세스 토큰이 만료되었습니다."), @@ -25,6 +27,7 @@ public enum ErrorCode { // 404 Not Found NOT_FOUND_END_POINT("A40400", HttpStatus.NOT_FOUND, "존재하지 않는 API입니다."), + USER_NOT_FOUND("A40401", HttpStatus.NOT_FOUND, "해당 유저는 존재하지 않습니다."), // 405 Method Not Allowed Error METHOD_NOT_ALLOWED("A40500", HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 메소드입니다."), diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java new file mode 100644 index 00000000..ea6d8686 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java @@ -0,0 +1,27 @@ +package com.jootalkpia.auth_server.user.controller; + +import com.jootalkpia.auth_server.client.dto.UserLoginRequest; +import com.jootalkpia.auth_server.user.dto.LoginSuccessResponse; +import com.jootalkpia.auth_server.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class UserController implements UserControllerDocs { + + private final UserService userService; + + @Override + @PostMapping("api/v1/user/login") + public ResponseEntity login( + @RequestParam final String authorizationCode, + @RequestBody final UserLoginRequest loginRequest + ) { + return ResponseEntity.ok().body(userService.create(authorizationCode, loginRequest)); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java new file mode 100644 index 00000000..be7d9fe7 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java @@ -0,0 +1,32 @@ +package com.jootalkpia.auth_server.user.controller; + + +import com.jootalkpia.auth_server.client.dto.UserLoginRequest; +import com.jootalkpia.auth_server.user.dto.LoginSuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "User", description = "User 관련 API") +public interface UserControllerDocs { + @Operation(summary = "소셜 로그인") + @ApiResponses( + value = { + @ApiResponse(responseCode = "A40004", description = "인가 코드가 만료되었습니다."), + @ApiResponse(responseCode = "A40005", description = "로그인 요청이 유효하지 않습니다."), + @ApiResponse(responseCode = "A40100", description = "액세스 토큰이 만료되었습니다."), + @ApiResponse(responseCode = "A40101", description = "리프레시 토큰이 만료되었습니다."), + @ApiResponse(responseCode = "A40102", description = "리프레시 토큰이 유효하지 않습니다."), + @ApiResponse(responseCode = "A40103", description = "유효하지 않은 토큰입니다."), + @ApiResponse(responseCode = "A40104", description = "해당 유저의 리프레시 토큰이 존재하지 않습니다.") + } + ) + ResponseEntity login( + @RequestParam final String authorizationCode, + @RequestBody final UserLoginRequest loginRequest + ); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java new file mode 100644 index 00000000..7d499e63 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java @@ -0,0 +1,21 @@ +package com.jootalkpia.auth_server.user.domain; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + protected LocalDateTime updatedAt; +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/SocialType.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/SocialType.java new file mode 100644 index 00000000..e97d6d19 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/SocialType.java @@ -0,0 +1,12 @@ +package com.jootalkpia.auth_server.user.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SocialType { + KAKAO("KAKAO"), + ; + private final String type; +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java new file mode 100644 index 00000000..633a4c31 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java @@ -0,0 +1,51 @@ +package com.jootalkpia.auth_server.user.domain; + + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import jakarta.persistence.Table; +import jakarta.persistence.Id; + +@Entity +@Getter +@Table(name = "users") +@Builder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class User extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long Id; + + private Long socialId; + + private String email; + + @Enumerated(EnumType.STRING) + private SocialType socialType; + + private String nickname; + + public static User of( + final Long socialId, + final String email, + final SocialType socialType, + final String socialNickname + ) { + return User.builder() + .socialId(socialId) + .email(email) + .socialType(socialType) + .nickname(socialNickname) + .build(); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/LoginSuccessResponse.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/LoginSuccessResponse.java new file mode 100644 index 00000000..c5f54fca --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/LoginSuccessResponse.java @@ -0,0 +1,18 @@ +package com.jootalkpia.auth_server.user.dto; + +public record LoginSuccessResponse( + + Long userId, + + String nickname, + + TokenDto token +) { + public static LoginSuccessResponse of( + final String nickname, + final Long userId, + final TokenDto token + ) { + return new LoginSuccessResponse(userId, nickname, token); + } +} \ No newline at end of file diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/TokenDto.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/TokenDto.java new file mode 100644 index 00000000..e6a07fda --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/dto/TokenDto.java @@ -0,0 +1,13 @@ +package com.jootalkpia.auth_server.user.dto; + +public record TokenDto( + + String accessToken, + + String refreshToken +) { + + public static TokenDto of(String accessToken, String refreshToken) { + return new TokenDto(accessToken, refreshToken); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java new file mode 100644 index 00000000..87111b6a --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java @@ -0,0 +1,25 @@ +package com.jootalkpia.auth_server.user.repository; + +import com.jootalkpia.auth_server.exception.CustomException; +import com.jootalkpia.auth_server.response.ErrorCode; +import com.jootalkpia.auth_server.user.domain.SocialType; +import com.jootalkpia.auth_server.user.domain.User; +import feign.Param; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface UserRepository extends JpaRepository, UserRepositoryCustom { + + @Query("SELECT u FROM User u WHERE u.socialId = :socialId AND u.socialType = :socialType") + Optional findUserBySocialTypeAndSocialId(@Param("socialId") Long socialId, + @Param("socialType") SocialType socialType); + + Optional findUserById(Long id); + + default User findUserByIdOrThrow(Long id) { + return findUserById(id) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + } +} + diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepositoryCustom.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepositoryCustom.java new file mode 100644 index 00000000..9555079e --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.jootalkpia.auth_server.user.repository; + +import com.jootalkpia.auth_server.user.domain.SocialType; +import com.jootalkpia.auth_server.user.domain.User; +import java.util.Optional; + +public interface UserRepositoryCustom { + Optional findUserBySocialTypeAndSocialId(final Long socialId, final SocialType socialType); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java new file mode 100644 index 00000000..d56d8638 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java @@ -0,0 +1,107 @@ +package com.jootalkpia.auth_server.user.service; + +import com.jootalkpia.auth_server.client.dto.UserInfoResponse; +import com.jootalkpia.auth_server.client.dto.UserLoginRequest; +import com.jootalkpia.auth_server.client.kakao.KakaoSocialService; +import com.jootalkpia.auth_server.exception.CustomException; +import com.jootalkpia.auth_server.jwt.JwtTokenProvider; +import com.jootalkpia.auth_server.jwt.TokenService; +import com.jootalkpia.auth_server.response.ErrorCode; +import com.jootalkpia.auth_server.security.UserAuthentication; +import com.jootalkpia.auth_server.user.domain.SocialType; +import com.jootalkpia.auth_server.user.domain.User; +import com.jootalkpia.auth_server.user.dto.LoginSuccessResponse; +import com.jootalkpia.auth_server.user.dto.TokenDto; +import com.jootalkpia.auth_server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; + private final KakaoSocialService kakaoSocialService; + + public LoginSuccessResponse create( + final String authorizationCode, + final UserLoginRequest loginRequest + ) { + User user = getUser(getUserInfoResponse(authorizationCode, loginRequest)); + + TokenDto tokenDto = getTokenDto(user); + + return LoginSuccessResponse.of(user.getNickname(), user.getId(), tokenDto); + } + + public UserInfoResponse getUserInfoResponse( + final String authorizationCode, + final UserLoginRequest loginRequest + ) { + switch (loginRequest.socialType()) { + case KAKAO: + return kakaoSocialService.login(authorizationCode, loginRequest); + default: + throw new CustomException(ErrorCode.SOCIAL_TYPE_BAD_REQUEST); + } + } + + public User createUser(final UserInfoResponse userResponse) { + User user = User.of( + userResponse.socialId(), + userResponse.email(), + userResponse.socialType(), + userResponse.socialNickname()+"#"+userResponse.socialId() + ); + return userRepository.save(user); + } + + public User getBySocialId( + final Long socialId, + final SocialType socialType + ) { + User user = userRepository.findUserBySocialTypeAndSocialId(socialId, socialType).orElseThrow( + () -> new CustomException(ErrorCode.USER_NOT_FOUND) + ); + return user; + } + + public TokenDto getTokenByUserId( + final Long id + ) { + // 사용자 정보 가져오기 + User user = userRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + UserAuthentication userAuthentication = new UserAuthentication(id.toString(), null); + + String refreshToken = jwtTokenProvider.issueRefreshToken(userAuthentication); + tokenService.saveRefreshToken(id, refreshToken); + return TokenDto.of( + jwtTokenProvider.issueAccessToken(userAuthentication), + refreshToken + ); + } + + private TokenDto getTokenDto( + final User user + ) { + return getTokenByUserId(user.getId()); + } + + private User getUser(final UserInfoResponse userResponse) { + if (isExistingUser(userResponse.socialId(), userResponse.socialType())) { + return getBySocialId(userResponse.socialId(), userResponse.socialType()); + } else { + return createUser(userResponse); + } + } + + private boolean isExistingUser( + final Long socialId, + final SocialType socialType + ) { + return userRepository.findUserBySocialTypeAndSocialId(socialId, socialType).isPresent(); + } +} diff --git a/src/backend/auth_server/src/main/resources/application.yml b/src/backend/auth_server/src/main/resources/application.yml index 100af4d4..f8033abc 100644 --- a/src/backend/auth_server/src/main/resources/application.yml +++ b/src/backend/auth_server/src/main/resources/application.yml @@ -4,5 +4,23 @@ spring: host: ${REDIS_HOST} port: ${REDIS_PORT} + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} + username: ${DB_USER} + password: ${DB_PASSWORD} + + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show_sql: true + jwt: secret: ${JWT_SECRET} + +kakao: + clientId: ${KAKAO_CLIENTID}