diff --git a/build.gradle.kts b/build.gradle.kts index b4613f2..3402080 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,6 @@ subprojects { implementation("org.springframework.boot:spring-boot-starter-actuator") // tools - implementation("org.springframework.boot:spring-boot-starter-validation") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") @@ -55,6 +54,4 @@ subprojects { useJUnitPlatform() } } -// -//version = "0.0.1-SNAPSHOT" diff --git a/plu-api/build.gradle.kts b/plu-api/build.gradle.kts index fc399e4..a09e21d 100644 --- a/plu-api/build.gradle.kts +++ b/plu-api/build.gradle.kts @@ -1,26 +1,38 @@ plugins { - kotlin("jvm") + kotlin("jvm") } tasks.jar { - enabled = false + enabled = false } dependencies { - implementation(project(":plu-domain")) - implementation(project(":plu-external")) - implementation(project(":plu-common")) + implementation(project(":plu-domain")) + implementation(project(":plu-external")) + implementation(project(":plu-common")) - // web - implementation("org.springframework.boot:spring-boot-starter-web") + // web + implementation("org.springframework.boot:spring-boot-starter-web") - // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.springframework.session:spring-session-data-redis") - implementation(kotlin("stdlib-jdk8")) + // validation + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") + + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.session:spring-session-data-redis") + implementation(kotlin("stdlib-jdk8")) + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") } repositories { - mavenCentral() + mavenCentral() } kotlin { - jvmToolchain(17) + jvmToolchain(17) } diff --git a/plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt b/plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt similarity index 95% rename from plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt rename to plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt index 20f4274..4ed30ce 100644 --- a/plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt @@ -1,4 +1,4 @@ -package com.th.plu +package com.th.plu.api import com.th.plu.common.PluCommonRoot import com.th.plu.domain.PluDomainRoot diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt new file mode 100644 index 0000000..95c6174 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt @@ -0,0 +1,24 @@ +package com.th.plu.api.config + +import com.th.plu.api.config.interceptor.AuthInterceptor +import com.th.plu.api.config.resolver.MemberIdResolver +import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig( + private val authInterceptor: AuthInterceptor, + private val memberIdResolver: MemberIdResolver +) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(authInterceptor) + } + + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(memberIdResolver) + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt new file mode 100644 index 0000000..f3176a5 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt @@ -0,0 +1,5 @@ +package com.th.plu.api.config.interceptor + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Auth diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt new file mode 100644 index 0000000..88661ef --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt @@ -0,0 +1,24 @@ +package com.th.plu.api.config.interceptor + +import com.th.plu.common.constant.JwtKey +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor + +@Component +class AuthInterceptor( + private val loginCheckHandler: LoginCheckHandler +) : HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + if (handler !is HandlerMethod) { + return true + } + handler.getMethodAnnotation(Auth::class.java) ?: return true + val memberId = loginCheckHandler.getMemberId(request) + request.setAttribute(JwtKey.MEMBER_ID, memberId) + return true + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt new file mode 100644 index 0000000..3139fd3 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt @@ -0,0 +1,23 @@ +package com.th.plu.api.config.interceptor + +import com.th.plu.api.service.auth.jwt.JwtHandler +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import jakarta.servlet.http.HttpServletRequest +import org.springframework.stereotype.Component + +@Component +class LoginCheckHandler( + private val jwtHandler: JwtHandler +) { + fun getMemberId(request: HttpServletRequest): Long { + val bearerToken: String? = request.getHeader("Authorization") + if (!bearerToken.isNullOrBlank() && bearerToken.startsWith("Bearer ")) { + val accessToken = bearerToken.substring("Bearer ".length) + if (jwtHandler.validateToken(accessToken)) { + return jwtHandler.getMemberIdFromJwt(accessToken) + } + } + throw UnauthorizedException(ErrorCode.UNAUTHORIZED_EXCEPTION, "잘못된 JWT $bearerToken 입니다.") + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt new file mode 100644 index 0000000..62699c8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt @@ -0,0 +1,5 @@ +package com.th.plu.api.config.resolver + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class MemberId diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt new file mode 100644 index 0000000..1043222 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt @@ -0,0 +1,37 @@ +package com.th.plu.api.config.resolver + +import com.th.plu.api.config.interceptor.Auth +import com.th.plu.common.constant.JwtKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.InternalServerException +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class MemberIdResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(MemberId::class.java) && Long::class.java == parameter.parameterType + } + + override fun resolveArgument( + parameter: MethodParameter, mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? + ): Any? { + parameter.getMethodAnnotation(Auth::class.java) + ?: throw InternalServerException( + ErrorCode.INTERNAL_SERVER_EXCEPTION, + "인증이 필요한 컨트롤러 입니다. @Auth 어노테이션을 붙여주세요." + ) + + return webRequest.getAttribute(JwtKey.MEMBER_ID, 0) + ?: throw InternalServerException( + ErrorCode.INTERNAL_SERVER_EXCEPTION, + "MEMBER_ID 를 가져오지 못했습니다. ($parameter.javaClass - $parameter.method)" + ) + } +} diff --git a/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt similarity index 58% rename from src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt rename to plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt index 3879d4e..695a36c 100644 --- a/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt @@ -1,10 +1,12 @@ package com.th.plu.api.config.swagger +import com.th.plu.api.config.resolver.MemberId import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme +import org.springdoc.core.utils.SpringDocUtils import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -19,19 +21,22 @@ class SwaggerConfig { @Bean fun openAPI(): OpenAPI { val info = Info() - .title(TITLE) - .description(DESCRIPTION) - .version(VERSION) + .title(TITLE) + .description(DESCRIPTION) + .version(VERSION) val securityScheme = SecurityScheme() - .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") - .`in`(SecurityScheme.In.HEADER).name("Authorization") + .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER).name("Authorization") val securityRequirement = SecurityRequirement().addList("Bearer Token") return OpenAPI() - .components(Components().addSecuritySchemes("Bearer Token", securityScheme)) - .security(listOf(securityRequirement)) - .info(info) + .components(Components().addSecuritySchemes("Bearer Token", securityScheme)) + .security(listOf(securityRequirement)) + .info(info) } + init { + SpringDocUtils.getConfig().addAnnotationsToIgnore(MemberId::class.java) + } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt deleted file mode 100644 index ca5de9a..0000000 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.th.plu.api.controller - -import com.fasterxml.jackson.databind.exc.InvalidFormatException -import com.th.plu.common.dto.response.ApiResponse -import com.th.plu.common.exception.code.ErrorCode -import com.th.plu.common.exception.model.BadGatewayException -import com.th.plu.common.exception.model.NotFoundException -import com.th.plu.common.exception.model.ValidationException -import org.slf4j.LoggerFactory -import org.springframework.http.HttpStatus -import org.springframework.http.converter.HttpMessageNotReadableException -import org.springframework.web.HttpMediaTypeException -import org.springframework.web.HttpRequestMethodNotSupportedException -import org.springframework.web.bind.MethodArgumentNotValidException -import org.springframework.web.bind.ServletRequestBindingException -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestControllerAdvice -import org.springframework.web.servlet.NoHandlerFoundException -import org.springframework.web.servlet.resource.NoResourceFoundException -import java.net.BindException - -@RestControllerAdvice -class ExceptionController { - private val log = LoggerFactory.getLogger(this.javaClass) - - /** - * 400 Bad Request - */ - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(ValidationException::class) - fun handleValidationException(exception: ValidationException): ApiResponse { - log.error(exception.message) - return ApiResponse.error(exception.errorCode) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(MethodArgumentNotValidException::class) - fun handleMethodArgumentNotValidException(exception: MethodArgumentNotValidException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(BindException::class) - fun handleBindException(exception: BindException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.BIND_EXCEPTION) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler( - value = [ - HttpMessageNotReadableException::class, - InvalidFormatException::class, - ServletRequestBindingException::class - ] - ) - fun handleInvalidFormatException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.INVALID_FORMAT_EXCEPTION) - } - - /** - * 404 Not Found - */ - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(NotFoundException::class) - fun handleNotFoundException(exception: NotFoundException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(exception.errorCode) - } - - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler( - value = [ - NoHandlerFoundException::class, - NoResourceFoundException::class] - ) - fun handleNotFoundEndpointException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.NOT_FOUND_ENDPOINT_EXCEPTION) - } - - /** - * 405 Method Not Supported - */ - @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) - @ExceptionHandler(HttpRequestMethodNotSupportedException::class) - fun handleHttpRequestMethodNotSupportedException(exception: HttpRequestMethodNotSupportedException): ApiResponse { - return ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED_EXCEPTION) - } - - /** - * 415 Unsupported Media Type - */ - @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) - @ExceptionHandler(HttpMediaTypeException::class) - fun handleHttpMediaTypeException(exception: HttpMediaTypeException): ApiResponse { - return ApiResponse.error(ErrorCode.UNSUPPORTED_MEDIA_TYPE) - } - - /** - * 500 Internal Server Error - */ - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ExceptionHandler(Exception::class) - fun handleInternalServerException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.INTERNAL_SERVER_EXCEPTION) - } - - /** - * 502 Bad Gateway - */ - @ResponseStatus(HttpStatus.BAD_GATEWAY) - @ExceptionHandler(BadGatewayException::class) - fun handleBadGatewayException(exception: BadGatewayException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(exception.errorCode) - } -} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt new file mode 100644 index 0000000..d98a762 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -0,0 +1,61 @@ +package com.th.plu.api.controller.auth + +import com.th.plu.api.config.interceptor.Auth +import com.th.plu.api.config.resolver.MemberId +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.controller.auth.dto.request.TokenRequestDto +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.api.service.auth.AuthServiceProvider +import com.th.plu.api.service.auth.CommonAuthService +import com.th.plu.api.service.auth.jwt.TokenService +import com.th.plu.common.dto.response.ApiResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Auth") +@RestController +@RequestMapping("/api") +class AuthController( + private val authServiceProvider: AuthServiceProvider, + private val tokenService: TokenService, + private val commonAuthService: CommonAuthService +) { + + @Operation(summary = "소셜 회원가입") + @PostMapping("/v1/auth/signup") + fun signup(@Valid @RequestBody request: SignupRequestDto): ApiResponse { + val authService = authServiceProvider.getAuthService(request.socialType) + val memberId = authService.signup(request) + val tokenInfo = tokenService.createTokenInfo(memberId) + return ApiResponse.success(tokenInfo) + } + + @Operation(summary = "소셜 로그인") + @PostMapping("/v1/auth/login") + fun login(@Valid @RequestBody request: LoginRequestDto): ApiResponse { + val authService = authServiceProvider.getAuthService(request.socialType) + val memberId = authService.login(request) + val tokenInfo = tokenService.createTokenInfo(memberId) + return ApiResponse.success(tokenInfo) + } + + @Operation(summary = "토큰 갱신") + @PostMapping("/v1/auth/refresh") + fun reissue(@Valid @RequestBody request: TokenRequestDto): ApiResponse { + return ApiResponse.success(tokenService.reissueToken(request)) + } + + @Operation(summary = "로그아웃") + @PostMapping("/v1/auth/logout") + @Auth + fun logout(@MemberId memberId: Long): ApiResponse { + commonAuthService.logout(memberId) + return ApiResponse.success() + } +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt new file mode 100644 index 0000000..0e69aa8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt @@ -0,0 +1,22 @@ +package com.th.plu.api.controller.auth.dto.request + +import com.th.plu.domain.domain.member.MemberSocialType +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull + + +data class LoginRequestDto( + + @field:Schema(description = "소셜 로그인 타입", example = "KAKAO") + @field:NotNull(message = "socialType 을 입력해주세요.") + val socialType: MemberSocialType, + + @field:Schema(description = "소셜 토큰", example = "eyJhbGciOiJIUzUxdfadfadsMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "token 을 입력해주세요.") + val token: String, + + @field:Schema(description = "FCM 토큰", example = "adfaffaffdfsfewvasdvasvdsvffsddauaiviajvasvavisavja") + @field:NotBlank(message = "fcmToken 을 입력해주세요.") + val fcmToken: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt new file mode 100644 index 0000000..c344edf --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt @@ -0,0 +1,38 @@ +package com.th.plu.api.controller.auth.dto.request + +import com.th.plu.api.controller.member.dto.request.CreateUserRequestDto +import com.th.plu.domain.domain.member.MemberSocialType +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size + + +data class SignupRequestDto( + + @field:Schema(description = "소셜 로그인 타입", example = "KAKAO") + @field:NotNull(message = "socialType 을 입력해주세요.") + val socialType: MemberSocialType, + + @field:Schema(description = "소셜 토큰", example = "eyJhbGciOiJIUzUxdfadfadsMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "token 을 입력해주세요.") + val token: String, + + @field:Schema(description = "FCM 토큰", example = "adfaffaffdfsfewvasdvasvdsvffsddauaiviajvasvavisavja") + @field:NotBlank(message = "fcmToken 을 입력해주세요.") + val fcmToken: String, + + @field:Schema(description = "닉네임", example = "둘리") + @field:NotBlank(message = "nickname 을 입력해주세요.") + @field:Size(max = 8, message = "nickname 은 8자 이내로 입력해주세요.") + val nickname: String +) { + fun toCreateUserDto(socialId: String): CreateUserRequestDto { + return CreateUserRequestDto( + socialId = socialId, + socialType = socialType, + fcmToken = fcmToken, + nickname = nickname + ) + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt new file mode 100644 index 0000000..672a894 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt @@ -0,0 +1,15 @@ +package com.th.plu.api.controller.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank + +data class TokenRequestDto( + + @field:Schema(description = "토큰 - accessToken", example = "eyJhbGciOiJIUzUxMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "accessToken 을 입력해주세요.") + val accessToken: String, + + @field:Schema(description = "토큰 - refreshToken", example = "eyJhbGciOiJIUzUxMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "refreshToken 을 입력해주세요.") + val refreshToken: String +) \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt new file mode 100644 index 0000000..f844702 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt @@ -0,0 +1,12 @@ +package com.th.plu.api.controller.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class TokenResponseDto( + + @field:Schema(description = "PLU JWT accessToken") + val accessToken: String, + + @field:Schema(description = "PLU JWT refreshToken") + val refreshToken: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt new file mode 100644 index 0000000..b08f08a --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt @@ -0,0 +1,10 @@ +package com.th.plu.api.controller.member.dto.request + +import com.th.plu.domain.domain.member.MemberSocialType + +data class CreateUserRequestDto( + val socialId: String, + val socialType: MemberSocialType, + val fcmToken: String, + val nickname: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt index e3d8e6e..bd2dd49 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt @@ -1,8 +1,8 @@ package com.th.plu.api.controller.notification import com.th.plu.api.controller.notification.dto.request.MessageSendRequest -import com.th.plu.api.dto.ApiResponse import com.th.plu.api.service.notification.NotificationService +import com.th.plu.common.dto.response.ApiResponse import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt new file mode 100644 index 0000000..87d44a3 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt @@ -0,0 +1,38 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.service.member.MemberService +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository +import com.th.plu.external.client.apple.AppleTokenProvider +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AppleAuthService( + private val appleTokenProvider: AppleTokenProvider, + private val memberService: MemberService, + private val memberRepository: MemberRepository +) : AuthService { + + companion object { + private val socialType: MemberSocialType = MemberSocialType.APPLE + } + + @Transactional + override fun signup(request: SignupRequestDto): Long { + val socialId = appleTokenProvider.getSocialIdFromIdToken(request.token) + return memberService.registerUser(request.toCreateUserDto(socialId)) + } + + @Transactional + override fun login(request: LoginRequestDto): Long { + val socialId = appleTokenProvider.getSocialIdFromIdToken(request.token) + val member = MemberServiceUtils.findMemberBySocialIdAndSocialType(memberRepository, socialId, socialType) + member.updateFcmToken(request.fcmToken) + return member.id!! + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt new file mode 100644 index 0000000..c600534 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt @@ -0,0 +1,11 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto + +interface AuthService { + + fun signup(request: SignupRequestDto): Long + fun login(request: LoginRequestDto): Long + +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt new file mode 100644 index 0000000..b35f720 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt @@ -0,0 +1,25 @@ +package com.th.plu.api.service.auth + +import com.th.plu.domain.domain.member.MemberSocialType +import jakarta.annotation.PostConstruct +import org.springframework.stereotype.Component + +@Component +class AuthServiceProvider( + private val appleAuthService: AppleAuthService, + private val kakaoAuthService: KakaoAuthService +) { + companion object { + val authServiceMap = mutableMapOf() + } + + @PostConstruct + fun initAuthServiceMap() { + authServiceMap[MemberSocialType.KAKAO] = kakaoAuthService + authServiceMap[MemberSocialType.APPLE] = appleAuthService + } + + fun getAuthService(socialType: MemberSocialType): AuthService { + return authServiceMap[socialType]!! + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt new file mode 100644 index 0000000..65901c9 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt @@ -0,0 +1,21 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.service.auth.jwt.JwtHandler +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.domain.domain.member.repository.MemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CommonAuthService( + private val memberRepository: MemberRepository, + private val jwtHandler: JwtHandler +) { + + @Transactional + fun logout(memberId: Long) { + val member = MemberServiceUtils.findMemberById(memberRepository, memberId) + jwtHandler.expireRefreshToken(member.id!!) + member.resetFcmToken() + } +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt new file mode 100644 index 0000000..09f430b --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt @@ -0,0 +1,38 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.service.member.MemberService +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository +import com.th.plu.external.client.kakao.KakaoApiCaller +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class KakaoAuthService( + private val kakaoApiCaller: KakaoApiCaller, + private val memberService: MemberService, + private val memberRepository: MemberRepository +) : AuthService { + + companion object { + private val socialType: MemberSocialType = MemberSocialType.KAKAO + } + + @Transactional + override fun signup(request: SignupRequestDto): Long { + val response = kakaoApiCaller.getProfileInfo(request.token) + return memberService.registerUser(request.toCreateUserDto(response.id)) + } + + @Transactional + override fun login(request: LoginRequestDto): Long { + val response = kakaoApiCaller.getProfileInfo(request.token) + val member = MemberServiceUtils.findMemberBySocialIdAndSocialType(memberRepository, response.id, socialType) + member.updateFcmToken(request.fcmToken) + return member.id!! + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt new file mode 100644 index 0000000..3c5a2b8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt @@ -0,0 +1,106 @@ +package com.th.plu.api.service.auth.jwt + +import com.th.plu.api.service.redis.RedisHandler +import com.th.plu.common.constant.JwtKey +import com.th.plu.common.constant.RedisKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import io.jsonwebtoken.* +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.DecodingException +import io.jsonwebtoken.security.Keys +import jakarta.annotation.PostConstruct +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.security.Key +import java.util.* + +@Component +class JwtHandler( + private val redisHandler: RedisHandler, +) { + @Value("\${jwt.secret}") + private var jwtSecret: String? = null + private var secretKey: Key? = null + private val log = LoggerFactory.getLogger(this.javaClass) + + companion object { + // private const val ACCESS_TOKEN_EXPIRE_TIME = 10 * 60 * 1000L // 10분 + // private const val REFRESH_TOKEN_EXPIRE_TIME = 6 * 30 * 24 * 60 * 60 * 1000L // 180일 + private const val ACCESS_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 + private const val REFRESH_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 + private const val EXPIRED_TIME = 1L + } + + @PostConstruct + fun init() { + val keyBytes: ByteArray = Decoders.BASE64.decode(jwtSecret) + this.secretKey = Keys.hmacShaKeyFor(keyBytes) + } + + fun createTokenInfo(memberId: Long): TokenDto { + val now = Date().time + val accessTokenExpiresIn = Date(now + ACCESS_TOKEN_EXPIRE_TIME) + val refreshTokenExpiresIn = Date(now + REFRESH_TOKEN_EXPIRE_TIME) + + // Access Token 생성 + val accessToken: String = Jwts.builder() + .claim(JwtKey.MEMBER_ID, memberId) + .setExpiration(accessTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact() + + // Refresh Token 생성 + val refreshToken: String = Jwts.builder() + .setExpiration(refreshTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact() + + redisHandler.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME) + + return TokenDto(accessToken, refreshToken) + } + + fun expireRefreshToken(memberId: Long) { + redisHandler.delete(RedisKey.REFRESH_TOKEN + memberId) + } + + fun validateToken(token: String?): Boolean { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token) + return true + } catch (e: SecurityException) { + log.warn("Invalid JWT Token", e) + } catch (e: MalformedJwtException) { + log.warn("Invalid JWT Token", e) + } catch (e: DecodingException) { + log.warn("Invalid JWT Token", e) + } catch (e: ExpiredJwtException) { + log.warn("Expired JWT Token", e) + } catch (e: UnsupportedJwtException) { + log.warn("Unsupported JWT Token", e) + } catch (e: IllegalArgumentException) { + log.warn("JWT claims string is empty.", e) + } catch (e: Exception) { + log.error("Unhandled JWT exception", e) + } + return false + } + + fun getMemberIdFromJwt(accessToken: String): Long { + val memberId = parseClaims(accessToken)[JwtKey.MEMBER_ID] as Int? + return memberId?.toLong() ?: throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "주어진 액세스 토큰 $accessToken 으로 유저 정보를 찾을 수 없습니다." + ) + } + + private fun parseClaims(accessToken: String): Claims { + return try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).body + } catch (e: ExpiredJwtException) { + e.claims + } + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt new file mode 100644 index 0000000..3de5aa9 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt @@ -0,0 +1,12 @@ +package com.th.plu.api.service.auth.jwt + +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto + +data class TokenDto( + val accessToken: String, + val refreshToken: String +) { + fun toResponseDto(): TokenResponseDto { + return TokenResponseDto(accessToken, refreshToken) + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt new file mode 100644 index 0000000..9733ea5 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt @@ -0,0 +1,55 @@ +package com.th.plu.api.service.auth.jwt + +import com.th.plu.api.controller.auth.dto.request.TokenRequestDto +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.api.service.redis.RedisHandler +import com.th.plu.common.constant.RedisKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import com.th.plu.domain.domain.member.repository.MemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TokenService( + private val jwtHandler: JwtHandler, + private val memberRepository: MemberRepository, + private val redisHandler: RedisHandler +) { + fun createTokenInfo(memberId: Long): TokenResponseDto { + val tokens = jwtHandler.createTokenInfo(memberId) + return tokens.toResponseDto() + } + + @Transactional + fun reissueToken(request: TokenRequestDto): TokenResponseDto { + val memberId = jwtHandler.getMemberIdFromJwt(request.accessToken) + val member = MemberServiceUtils.findMemberById(memberRepository, memberId) + if (!jwtHandler.validateToken(request.refreshToken)) { + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "주어진 리프레시 토큰 ${request.refreshToken} 이 유효하지 않습니다." + ) + } + val refreshToken = redisHandler.get(RedisKey.REFRESH_TOKEN + memberId) + if (refreshToken == null) { + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "이미 만료된 리프레시 토큰 ${request.refreshToken} 입니다." + ) + } + if (refreshToken != request.refreshToken) { + jwtHandler.expireRefreshToken(member.id!!) + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "해당 리프레시 토큰 ${request.refreshToken} 의 정보가 일치하지 않습니다." + ) + } + val newTokens = jwtHandler.createTokenInfo(memberId) + return newTokens.toResponseDto() + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt new file mode 100644 index 0000000..cb812a0 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt @@ -0,0 +1,42 @@ +package com.th.plu.api.service.member + +import com.th.plu.api.controller.member.dto.request.CreateUserRequestDto +import com.th.plu.domain.domain.member.Member +import com.th.plu.domain.domain.member.Onboarding +import com.th.plu.domain.domain.member.Setting +import com.th.plu.domain.domain.member.repository.MemberRepository +import com.th.plu.domain.domain.member.repository.OnboardingRepository +import com.th.plu.domain.domain.member.repository.SettingRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class MemberService( + private val memberRepository: MemberRepository, + private val onboardingRepository: OnboardingRepository, + private val settingRepository: SettingRepository +) { + + @Transactional + fun registerUser(request: CreateUserRequestDto): Long { + MemberServiceUtils.validateNotExistsMember(memberRepository, request.socialId, request.socialType) + // TODO: 닉네임 중복 체크 추가해야합니다. + val member = memberRepository.save( + Member.newInstance( + socialId = request.socialId, + socialType = request.socialType, + fcmToken = request.fcmToken, + setting = settingRepository.save(Setting.newInstance()) + ) + ) + val onboarding = onboardingRepository.save( + Onboarding.newInstance( + member = member, + nickname = request.nickname + ) + ) + member.initOnboarding(onboarding) + return member.id!! + } + +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt new file mode 100644 index 0000000..9bff5b9 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt @@ -0,0 +1,39 @@ +package com.th.plu.api.service.member + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.ConflictException +import com.th.plu.common.exception.model.NotFoundException +import com.th.plu.domain.domain.member.Member +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository + +class MemberServiceUtils { + companion object { + fun validateNotExistsMember( + memberRepository: MemberRepository, + socialId: String, + socialType: MemberSocialType + ) { + if (memberRepository.existBySocialIdAndSocialType(socialId, socialType)) { + throw ConflictException(ErrorCode.CONFLICT_MEMBER_EXCEPTION, "이미 존재하는 유저 $socialId - $socialType 입니다") + } + } + + fun findMemberById( + memberRepository: MemberRepository, + id: Long + ): Member { + return memberRepository.findMemberById(id) + ?: throw NotFoundException(ErrorCode.NOT_FOUND_MEMBER_EXCEPTION, "존재하지 않는 유저 $id 입니다") + } + + fun findMemberBySocialIdAndSocialType( + memberRepository: MemberRepository, + socialId: String, + socialType: MemberSocialType + ): Member { + return memberRepository.findMemberBySocialIdAndSocialType(socialId, socialType) + ?: throw NotFoundException(ErrorCode.NOT_FOUND_MEMBER_EXCEPTION, "존재하지 않는 유저 $socialId $socialType 입니다") + } + } +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt new file mode 100644 index 0000000..92eacca --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt @@ -0,0 +1,22 @@ +package com.th.plu.api.service.redis + +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Component +class RedisHandler( + private val redisTemplate: RedisTemplate +) { + fun get(key: String): String? { + return redisTemplate.opsForValue().get(key) as String? + } + + fun set(key: String, value: Any, timeout: Long) { + redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MILLISECONDS) + } + + fun delete(key: String) { + redisTemplate.delete(key) + } +} \ No newline at end of file diff --git a/plu-api/src/main/resources/application-dev.yml b/plu-api/src/main/resources/application-dev.yml index 38bbdbd..2c91d1d 100644 --- a/plu-api/src/main/resources/application-dev.yml +++ b/plu-api/src/main/resources/application-dev.yml @@ -49,5 +49,5 @@ spring: config: path: ${FIREBASE_PATH} -#jwt: -# secret: ${JWT_SECRET_DEV} +jwt: + secret: ${JWT_SECRET_DEV} diff --git a/plu-api/src/main/resources/application-local.yml b/plu-api/src/main/resources/application-local.yml index 376a74c..577ae0c 100644 --- a/plu-api/src/main/resources/application-local.yml +++ b/plu-api/src/main/resources/application-local.yml @@ -48,5 +48,5 @@ spring: config: path: ${FIREBASE_PATH} -#jwt: -# secret: secretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKey +jwt: + secret: secretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKey diff --git a/plu-api/src/main/resources/sql/schema.sql b/plu-api/src/main/resources/sql/schema.sql index c747aea..bc80224 100644 --- a/plu-api/src/main/resources/sql/schema.sql +++ b/plu-api/src/main/resources/sql/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE `members` `social_type` varchar(30) NOT NULL, `member_role` varchar(30) NOT NULL, `fcm_token` varchar(300) NULL, + `setting_id` bigint NOT NULL, `created_at` datetime NOT NULL, `modified_at` datetime NOT NULL ); @@ -19,7 +20,6 @@ CREATE TABLE `members` CREATE TABLE `settings` ( `setting_id` bigint auto_increment primary key, - `member_id` bigint NOT NULL, `notification_status` boolean NOT NULL, `created_at` datetime NOT NULL, `modified_at` datetime NOT NULL diff --git a/plu-common/build.gradle.kts b/plu-common/build.gradle.kts index d991fb4..f79ba45 100644 --- a/plu-common/build.gradle.kts +++ b/plu-common/build.gradle.kts @@ -2,11 +2,11 @@ plugins { kotlin("jvm") } tasks.jar { - enabled = true + enabled = true } tasks.bootJar { - enabled = false + enabled = false } diff --git a/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt b/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt index 3effb00..9746400 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt @@ -3,12 +3,11 @@ package com.th.plu.common.aop.advice import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.th.plu.common.dto.response.ApiResponse import com.th.plu.common.exception.code.ErrorCode -import com.th.plu.common.exception.model.BadGatewayException -import com.th.plu.common.exception.model.NotFoundException -import com.th.plu.common.exception.model.ValidationException +import com.th.plu.common.exception.model.* import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.validation.BindException import org.springframework.web.HttpMediaTypeException import org.springframework.web.HttpRequestMethodNotSupportedException import org.springframework.web.bind.MethodArgumentNotValidException @@ -18,7 +17,6 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.servlet.NoHandlerFoundException import org.springframework.web.servlet.resource.NoResourceFoundException -import java.net.BindException @RestControllerAdvice class ExceptionControllerAdvice { @@ -38,14 +36,20 @@ class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException::class) fun handleMethodArgumentNotValidException(exception: MethodArgumentNotValidException): ApiResponse { log.error(exception.message, exception); - return ApiResponse.error(ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION); + return ApiResponse.error( + ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, + exception.bindingResult.fieldError?.defaultMessage.toString() + ) } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException::class) fun handleBindException(exception: BindException): ApiResponse { log.error(exception.message, exception); - return ApiResponse.error(ErrorCode.BIND_EXCEPTION); + return ApiResponse.error( + ErrorCode.BIND_EXCEPTION, + exception.bindingResult.fieldError?.defaultMessage.toString() + ) } @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -61,6 +65,26 @@ class ExceptionControllerAdvice { return ApiResponse.error(ErrorCode.INVALID_FORMAT_EXCEPTION); } + /** + * 401 Unauthorized + */ + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException::class) + fun handleUnauthorizedException(exception: UnauthorizedException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + + /** + * 403 Forbidden + */ + @ResponseStatus(HttpStatus.FORBIDDEN) + @ExceptionHandler(ForbiddenException::class) + fun handleForbiddenException(exception: ForbiddenException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + /** * 404 Not Found */ @@ -91,6 +115,16 @@ class ExceptionControllerAdvice { return ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED_EXCEPTION) } + /** + * 409 Conflict + */ + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(ConflictException::class) + fun handleConflictException(exception: ConflictException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + /** * 415 Unsupported Media Type */ diff --git a/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt b/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt new file mode 100644 index 0000000..364c430 --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.constant + +object JwtKey { + const val MEMBER_ID = "MEMBER_ID" +} diff --git a/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt b/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt new file mode 100644 index 0000000..c0bbb9c --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.constant + +object RedisKey { + const val REFRESH_TOKEN = "RT:" +} diff --git a/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt b/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt index 57060f2..0fa671e 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt @@ -17,5 +17,9 @@ data class ApiResponse(val code: String, val message: String, var data: T?) { fun error(errorCode: ErrorCode): ApiResponse { return ApiResponse(errorCode.code, errorCode.message, null) } + + fun error(errorCode: ErrorCode, message: String): ApiResponse { + return ApiResponse(errorCode.code, message, null) + } } } diff --git a/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt b/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt new file mode 100644 index 0000000..5a8801e --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.exception.model + +import com.th.plu.common.exception.code.ErrorCode + +class UnauthorizedException(errorCode: ErrorCode, message: String) : PluException(errorCode, message) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt index 6bb639b..1bf7984 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt @@ -1,9 +1,13 @@ package com.th.plu.domain.config.jpa +import com.th.plu.domain.PluDomainRoot +import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.context.annotation.Configuration import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.data.jpa.repository.config.EnableJpaRepositories @Configuration +@EntityScan(basePackageClasses = [PluDomainRoot::class]) +@EnableJpaRepositories(basePackageClasses = [PluDomainRoot::class]) @EnableJpaAuditing -class JpaConfig { -} +class JpaConfig diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt index abb3e22..f6e5785 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt @@ -8,7 +8,6 @@ import org.springframework.context.annotation.Configuration @Configuration class QueryDslConfig( - @PersistenceContext private val entityManager: EntityManager ) { diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt index 8368a85..d3667fb 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt @@ -16,11 +16,10 @@ open class BaseEntity( @CreatedDate @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Column(name = "created_at", nullable = false) - val createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = LocalDateTime.now(), @LastModifiedDate @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Column(name = "modified_at", nullable = false) - val modifiedAt: LocalDateTime = LocalDateTime.now(), -) { -} + var modifiedAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt index f75174d..72349c4 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt @@ -8,28 +8,63 @@ import jakarta.persistence.* @Entity class Member( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") - val id: Long? = null, + var id: Long? = null, - @Embedded - var socialInfo: SocialInfo, + @Column(name = "social_id", nullable = false, length = 300) + var socialId: String, + + @Column(name = "social_type", nullable = false, length = 30) + @Enumerated(EnumType.STRING) + var socialType: MemberSocialType, @Column(name = "member_role", nullable = false, length = 30) @Enumerated(EnumType.STRING) var role: MemberRole, - @Column(name = "fcm_token", nullable = false, length = 300) - var fcmToken: String, + @Column(name = "fcm_token", nullable = true, length = 300) + var fcmToken: String?, - @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) + @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) + @JoinColumn(name = "setting_id", nullable = false) var setting: Setting, @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) - var onboarding: Onboarding, + var onboarding: Onboarding?, @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) - var answers: List, + var answers: List = mutableListOf() + +) : BaseEntity() { + + companion object { + fun newInstance( + socialId: String, socialType: MemberSocialType, fcmToken: String, + setting: Setting + ): Member { + return Member( + id = null, + socialId = socialId, + socialType = socialType, + role = MemberRole.MEMBER, + fcmToken = fcmToken, + setting = setting, + onboarding = null + ) + } + } + + fun initOnboarding(onboarding: Onboarding) { + this.onboarding = onboarding + } + + fun updateFcmToken(fcmToken: String) { + this.fcmToken = fcmToken + } - ) : BaseEntity() { + fun resetFcmToken() { + this.fcmToken = null + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt index 8d3665f..5c157d2 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt @@ -7,12 +7,27 @@ import jakarta.persistence.* @Entity class Onboarding( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "onboarding_id") - val id: Long? = null, + var id: Long? = null, @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) @JoinColumn(name = "member_id", nullable = false) var member: Member, + + @Column(name = "nickname", nullable = false, length = 30) + var nickname: String, ) : BaseEntity() { + + companion object { + + fun newInstance(member: Member, nickname: String): Onboarding { + return Onboarding( + id = null, + member = member, + nickname = nickname + ) + } + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt index 04b970a..834c661 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt @@ -7,16 +7,22 @@ import jakarta.persistence.* @Entity class Setting( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "setting_id") - val id: Long? = null, + var id: Long? = null, @Column(name = "notification_status", nullable = false) - var notificationStatus: Boolean, - - @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) - @JoinColumn(name = "member_id", nullable = false) - var member: Member + var notificationStatus: Boolean ) : BaseEntity() { + + companion object { + fun newInstance(): Setting { + return Setting( + id = null, + notificationStatus = false + ) + } + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt deleted file mode 100644 index e9801ec..0000000 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.th.plu.domain.domain.member - -import jakarta.persistence.* -import lombok.AccessLevel -import lombok.AllArgsConstructor -import lombok.EqualsAndHashCode -import lombok.Getter -import lombok.NoArgsConstructor - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@EqualsAndHashCode -@Embeddable -class SocialInfo( - - @Column(name = "social_id", nullable = false, length = 300) - val socialId: String, - - @Column(name = "social_type", nullable = false, length = 30) - @Enumerated(EnumType.STRING) - val socialType : MemberSocialType -) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt index b369995..5c20ea1 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt @@ -19,8 +19,8 @@ class MemberRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberRe return queryFactory .selectFrom(member) .where( - member.socialInfo.socialId.eq(socialId), - member.socialInfo.socialType.eq(socialType) + member.socialId.eq(socialId), + member.socialType.eq(socialType) ).fetchOne() } @@ -28,8 +28,8 @@ class MemberRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberRe return queryFactory .selectFrom(member) .where( - member.socialInfo.socialId.eq(socialId), - member.socialInfo.socialType.eq(socialType) + member.socialId.eq(socialId), + member.socialType.eq(socialType) ).fetchOne() != null } } diff --git a/plu-external/build.gradle.kts b/plu-external/build.gradle.kts index 3e76096..66125d7 100644 --- a/plu-external/build.gradle.kts +++ b/plu-external/build.gradle.kts @@ -1,31 +1,33 @@ plugins { - kotlin("jvm") + kotlin("jvm") } tasks.jar { - enabled = true + enabled = true } tasks.bootJar { - enabled = false + enabled = false } dependencies { - implementation(project(":plu-common")) + implementation(project(":plu-common")) - // SQS - api(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")) - api("io.awspring.cloud:spring-cloud-aws-starter-sqs") + // SQS + api(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")) + api("io.awspring.cloud:spring-cloud-aws-starter-sqs") + // WebFlux + implementation("org.springframework.boot:spring-boot-starter-webflux") - // WebFlux - implementation("org.springframework.boot:spring-boot-starter-webflux") + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.2") - implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("stdlib-jdk8")) } repositories { - mavenCentral() + mavenCentral() } kotlin { - jvmToolchain(17) + jvmToolchain(17) } diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt new file mode 100644 index 0000000..82c4dfe --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt @@ -0,0 +1,8 @@ +package com.th.plu.external.client.apple + +import com.th.plu.external.client.apple.dto.response.AppleProfileResponseDto + +interface AppleApiCaller { + + fun getProfileInfo(): AppleProfileResponseDto +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt new file mode 100644 index 0000000..8447bc3 --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt @@ -0,0 +1,54 @@ +package com.th.plu.external.client.apple + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import io.jsonwebtoken.Jwts +import org.springframework.stereotype.Component +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + +@Component +class AppleTokenProvider( + private val appleApiCaller: AppleApiCaller, + private val objectMapper: ObjectMapper +) { + + fun getSocialIdFromIdToken(idToken: String): String { + val headerIdToken = idToken.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + return try { + val header = objectMapper.readValue( + String(Base64.getDecoder().decode(headerIdToken), StandardCharsets.UTF_8), + object : TypeReference>() {}) + val publicKey: PublicKey = getPublicKey(header) + val claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body + claims.subject // return socialId + } catch (e: Exception) { + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "잘못된 애플 idToken $idToken 입니다 (reason: ${e.message})" + ) + } + } + + private fun getPublicKey(header: Map): PublicKey { + val response = appleApiCaller.getProfileInfo() + val key = response.getMatchedPublicKey(header["kid"].toString(), header["alg"].toString()) + val nBytes: ByteArray = Base64.getUrlDecoder().decode(key.n) + val eBytes: ByteArray = Base64.getUrlDecoder().decode(key.e) + val nBigInt = BigInteger(1, nBytes) + val eBigInt = BigInteger(1, eBytes) + val publicKeySpec = RSAPublicKeySpec(nBigInt, eBigInt) + val keyFactory = KeyFactory.getInstance(key.kty) + return keyFactory.generatePublic(publicKeySpec) + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt new file mode 100644 index 0000000..821500e --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt @@ -0,0 +1,28 @@ +package com.th.plu.external.client.apple + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException +import com.th.plu.external.client.apple.dto.response.AppleProfileResponseDto +import org.springframework.http.HttpStatusCode +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class WebClientAppleCaller( + private val webClient: WebClient +) : AppleApiCaller { + + override fun getProfileInfo(): AppleProfileResponseDto { + return webClient.get() + .uri("https://appleid.apple.com/auth/keys") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + .onStatus(HttpStatusCode::is5xxServerError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + .bodyToMono(AppleProfileResponseDto::class.java) + .block()!! + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt new file mode 100644 index 0000000..5f254ef --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt @@ -0,0 +1,25 @@ +package com.th.plu.external.client.apple.dto.response + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException + +data class AppleProfileResponseDto( + val keys: List +) { + class Key { + val alg: String? = null + val e: String? = null + val kid: String? = null + val kty: String? = null + val n: String? = null + val use: String? = null + } + + fun getMatchedPublicKey(kid: String, alg: String): Key { + return try { + this.keys.first { it.kid.equals(kid) && it.alg.equals(alg) } + } catch (_: NoSuchElementException) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt new file mode 100644 index 0000000..15b8040 --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt @@ -0,0 +1,8 @@ +package com.th.plu.external.client.kakao + +import com.th.plu.external.client.kakao.dto.response.KakaoProfileResponseDto + +interface KakaoApiCaller { + + fun getProfileInfo(accessToken: String): KakaoProfileResponseDto +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt new file mode 100644 index 0000000..449822f --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt @@ -0,0 +1,33 @@ +package com.th.plu.external.client.kakao + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException +import com.th.plu.common.exception.model.ValidationException +import com.th.plu.external.client.kakao.dto.response.KakaoProfileResponseDto +import org.springframework.http.HttpStatusCode +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class WebClientKakaoCaller( + private val webClient: WebClient +) : KakaoApiCaller { + + override fun getProfileInfo(accessToken: String): KakaoProfileResponseDto { + return webClient.get() + .uri("https://kapi.kakao.com/v2/user/me") + .headers { it.setBearerAuth(accessToken) } + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { + throw ValidationException( + ErrorCode.VALIDATION_INVALID_TOKEN_EXCEPTION, + "잘못된 카카오 액세스 토큰 $accessToken 입니다." + ) + } + .onStatus(HttpStatusCode::is5xxServerError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "카카오 로그인 연동 중 에러가 발생하였습니다.") + } + .bodyToMono(KakaoProfileResponseDto::class.java) + .block()!! + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt new file mode 100644 index 0000000..d0122db --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt @@ -0,0 +1,5 @@ +package com.th.plu.external.client.kakao.dto.response + +data class KakaoProfileResponseDto( + val id: String +) diff --git a/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt index e59d3ee..e21ef0c 100644 --- a/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt +++ b/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt @@ -1,5 +1,5 @@ package com.th.plu.external.sqs.dto -data class FirebaseMessageDto(val type: MessageType, val fcmToken: String, val title: String, val body: String) : +data class FirebaseMessageDto(val type: MessageType, val fcmToken: String?, val title: String, val body: String) : SqsMessageDto(type) { } diff --git a/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt b/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt index 7c8e527..34e35c8 100644 --- a/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt +++ b/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt @@ -25,9 +25,11 @@ class FirebaseCloudMessageService( private val log = LoggerFactory.getLogger(this.javaClass) private val LOG_PREFIX = "====> [Firebase Cloud Message]" - fun sendMessageTo(fcmToken: String, title: String, body: String) { - val message = makeMessage(fcmToken, title, body) - firebaseApiCaller.requestFcmMessaging(getAccessToken(), message) + fun sendMessageTo(fcmToken: String?, title: String, body: String) { + if (fcmToken != null) { + val message = makeMessage(fcmToken, title, body) + firebaseApiCaller.requestFcmMessaging(getAccessToken(), message) + } } fun makeMessage(fcmToken: String, title: String, body: String): String { @@ -45,7 +47,10 @@ class FirebaseCloudMessageService( return googleCredentials.accessToken.tokenValue } catch (exception: Exception) { log.error(exception.message, exception) - throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "${LOG_PREFIX} FCM Access Token 발급 과정에서 에러가 발생하였습니다.") + throw BadGatewayException( + ErrorCode.BAD_GATEWAY_EXCEPTION, + "${LOG_PREFIX} FCM Access Token 발급 과정에서 에러가 발생하였습니다." + ) } } }