Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import org.yapp.apis.auth.dto.request.SocialLoginRequest
import org.yapp.apis.auth.dto.request.TermsAgreementRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.request.WithdrawRequest
import org.yapp.apis.auth.dto.response.AuthResponse
import org.yapp.apis.auth.dto.response.UserProfileResponse
import org.yapp.apis.auth.usecase.AuthUseCase
import java.util.*

Expand Down Expand Up @@ -36,18 +35,12 @@ class AuthController(
return ResponseEntity.noContent().build()
}

@GetMapping("/me")
override fun getUserProfile(@AuthenticationPrincipal userId: UUID): ResponseEntity<UserProfileResponse> {
val userProfile = authUseCase.getUserProfile(userId)
return ResponseEntity.ok(userProfile)
}

@PutMapping("/terms-agreement")
override fun updateTermsAgreement(
@DeleteMapping("/withdraw")
override fun withdraw(
@AuthenticationPrincipal userId: UUID,
@Valid @RequestBody request: TermsAgreementRequest
): ResponseEntity<UserProfileResponse> {
val userProfile = authUseCase.updateTermsAgreement(userId, request.validTermsAgreed())
return ResponseEntity.ok(userProfile)
@Valid @RequestBody request: WithdrawRequest
): ResponseEntity<Unit> {
authUseCase.withdraw(userId, request)
return ResponseEntity.noContent().build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.yapp.apis.auth.dto.request.SocialLoginRequest
import org.yapp.apis.auth.dto.request.TermsAgreementRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.request.WithdrawRequest
import org.yapp.apis.auth.dto.response.AuthResponse
import org.yapp.apis.auth.dto.response.UserProfileResponse
import org.yapp.globalutils.exception.ErrorResponse
import java.util.*

Expand Down Expand Up @@ -90,42 +88,28 @@ interface AuthControllerApi {
@PostMapping("/signout")
fun signOut(@AuthenticationPrincipal userId: UUID): ResponseEntity<Unit>

@Operation(summary = "사용자 프로필 조회", description = "현재 로그인한 사용자의 프로필 정보를 조회합니다.")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "사용자 프로필 조회 성공",
content = [Content(schema = Schema(implementation = UserProfileResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "사용자를 찾을 수 없음",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
@Operation(
summary = "회원 탈퇴",
description = "사용자 계정을 탈퇴합니다."
)
@GetMapping("/me")
fun getUserProfile(@AuthenticationPrincipal userId: UUID): ResponseEntity<UserProfileResponse>

@Operation(summary = "약관 동의 상태 수정", description = "사용자의 약관 동의 상태를 업데이트합니다")
@ApiResponses(
value = [
ApiResponse(responseCode = "204", description = "회원 탈퇴 성공"),
ApiResponse(
responseCode = "200",
description = "약관 동의 상태 업데이트 성공",
content = [Content(schema = Schema(implementation = UserProfileResponse::class))]
responseCode = "400",
description = "잘못된 요청 또는 사용자를 찾을 수 없음",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "사용자를 찾을 수 없음",
responseCode = "500",
description = "Apple or Kakao 서버 연결 해제 실패",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@PutMapping("/terms-agreement")
fun updateTermsAgreement(
@DeleteMapping("/withdraw")
fun withdraw(
@AuthenticationPrincipal userId: UUID,
@Valid @RequestBody request: TermsAgreementRequest
): ResponseEntity<UserProfileResponse>
@Valid @RequestBody request: WithdrawRequest
): ResponseEntity<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package org.yapp.apis.auth.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotNull
import org.yapp.apis.auth.dto.response.CreateUserResponse
import org.yapp.apis.auth.dto.response.UserAuthInfoResponse
import org.yapp.apis.user.dto.response.CreateUserResponse
import org.yapp.apis.user.dto.response.UserAuthInfoResponse
import org.yapp.globalutils.auth.Role
import java.util.UUID

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package org.yapp.apis.auth.dto.request
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import org.yapp.apis.auth.dto.response.CreateUserResponse
import org.yapp.apis.auth.strategy.AppleAuthCredentials
import org.yapp.apis.user.dto.response.CreateUserResponse
import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials
import java.util.UUID

@Schema(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package org.yapp.apis.auth.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import org.yapp.apis.auth.strategy.AppleAuthCredentials
import org.yapp.apis.auth.strategy.AuthCredentials
import org.yapp.apis.auth.strategy.KakaoAuthCredentials
import org.yapp.apis.auth.exception.AuthErrorCode
import org.yapp.apis.auth.exception.AuthException
import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials
import org.yapp.apis.auth.strategy.signin.KakaoAuthCredentials
import org.yapp.apis.auth.strategy.signin.SignInCredentials
import org.yapp.domain.user.ProviderType

@Schema(
Expand Down Expand Up @@ -41,7 +41,7 @@ data class SocialLoginRequest private constructor(
fun validOauthToken(): String = oauthToken!!

companion object {
fun toCredentials(request: SocialLoginRequest): AuthCredentials {
fun toCredentials(request: SocialLoginRequest): SignInCredentials {
val provider = try {
ProviderType.valueOf(request.validProviderType().uppercase())
} catch (e: IllegalArgumentException) {
Expand All @@ -55,7 +55,10 @@ data class SocialLoginRequest private constructor(
ProviderType.KAKAO -> KakaoAuthCredentials(request.validOauthToken())
ProviderType.APPLE -> {
val authCode = request.authorizationCode
?: throw AuthException(AuthErrorCode.INVALID_REQUEST, "Apple login requires an authorization code.")
?: throw AuthException(
AuthErrorCode.INVALID_REQUEST,
"Apple login requires an authorization code."
)
AppleAuthCredentials(request.validOauthToken(), authCode)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.yapp.apis.auth.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import org.yapp.domain.user.ProviderType

@Schema(
name = "WithdrawRequest",
description = "DTO for user withdrawal requests"
)
data class WithdrawRequest private constructor(
@Schema(
description = "Type of social login provider for withdrawal",
example = "APPLE",
required = true
)
@field:NotNull(message = "Provider type is not-null")
val providerType: ProviderType? = null
) {
fun validProviderType(): ProviderType = providerType!!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.yapp.apis.auth.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import org.yapp.apis.user.dto.response.WithdrawTargetUserResponse
import org.yapp.domain.user.ProviderType
import java.util.*

@Schema(description = "회원 탈퇴 처리 시 내부적으로 사용되는 요청 DTO")
data class WithdrawStrategyRequest private constructor(
@Schema(
description = "사용자 고유 ID",
example = "123e4567-e89b-12d3-a456-426614174000",
)
val userId: UUID,

@Schema(
description = "소셜 로그인 제공자 타입",
example = "KAKAO",
)
val providerType: ProviderType,

@Schema(
description = "소셜 로그인 제공자로부터 발급받은 고유 ID",
example = "21412412412",
)
val providerId: String,

@Schema(
description = "Apple 로그인 시 발급받은 리프레시 토큰 (Apple 로그인 회원 탈퇴 시에만 필요)",
example = "r_abc123def456ghi789jkl0mnopqrstu",
required = false
)
val appleRefreshToken: String?
) {
companion object {
fun from(response: WithdrawTargetUserResponse): WithdrawStrategyRequest {
return WithdrawStrategyRequest(
userId = response.id,
providerType = response.providerType,
providerId = response.providerId,
appleRefreshToken = response.appleRefreshToken
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ enum class AuthErrorCode(
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_08", "사용자를 찾을 수 없습니다."),
EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_09", "이메일을 찾을 수 없습니다."),
INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_400_10", "유효하지 않은 Apple ID 토큰입니다."),
PROVIDER_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_400_11", "요청된 공급자 타입이 실제 사용자의 공급자 타입과 일치하지 않습니다."),
APPLE_REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, "AUTH_400_12", "Apple 사용자 탈퇴 시 리프레시 토큰이 누락되었습니다."),

/* 401 UNAUTHORIZED */
INVALID_OAUTH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401_01", "잘못된 소셜 OAuth 토큰입니다."),
Expand Down
19 changes: 19 additions & 0 deletions apis/src/main/kotlin/org/yapp/apis/auth/manager/AppleApiManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,23 @@ class AppleApiManager(
)
}
}

fun revokeToken(appleRefreshToken: String) {
val clientSecret = appleClientSecretGenerator.generateClientSecret()

appleApi.revokeAppleToken(
clientId = properties.clientId,
clientSecret = clientSecret,
token = appleRefreshToken,
tokenTypeHint = "refresh_token"
).onSuccess {
log.info { "Successfully revoked Apple token." }
}.onFailure { originalError ->
log.error(originalError) { "Failed to revoke Apple token." }
throw AuthException(
AuthErrorCode.OAUTH_SERVER_ERROR,
"Failed to revoke Apple token: ${originalError.message}"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
package org.yapp.apis.auth.service

import jakarta.validation.Valid
import org.springframework.stereotype.Service
import org.springframework.validation.annotation.Validated
import org.yapp.apis.auth.dto.request.SaveAppleRefreshTokenRequest
import org.yapp.apis.auth.exception.AuthErrorCode
import org.yapp.apis.auth.exception.AuthException
import org.yapp.apis.auth.manager.AppleApiManager
import org.yapp.domain.user.UserDomainService
import org.yapp.infra.external.oauth.apple.response.AppleTokenResponse

@Service
@Validated
class AppleAuthService(
private val appleApiManager: AppleApiManager,
private val userDomainService: UserDomainService
) {
fun saveAppleRefreshTokenIfMissing(@Valid request: SaveAppleRefreshTokenRequest) {
if (request.appleRefreshToken == null) {
val tokenResponse = appleApiManager.fetchAppleOauthTokens(request.validAuthorizationCode())
fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse {
val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode)

val refreshToken = tokenResponse.refreshToken
?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
tokenResponse.refreshToken
?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)

userDomainService.updateAppleRefreshToken(request.validUserId(), refreshToken)
}
return tokenResponse
}
Comment on lines +15 to 22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

불필요한 null 체크 로직 간소화 가능

refreshToken의 존재 여부만 확인하고 전체 tokenResponse를 반환하는 현재 로직은 다음과 같이 간소화할 수 있습니다:

 fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse {
-    val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode)
-
-    tokenResponse.refreshToken
-        ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
-
-    return tokenResponse
+    return appleApiManager.fetchAppleOauthTokens(authorizationCode).also {
+        it.refreshToken ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse {
val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode)
val refreshToken = tokenResponse.refreshToken
?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
tokenResponse.refreshToken
?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
userDomainService.updateAppleRefreshToken(request.validUserId(), refreshToken)
}
return tokenResponse
}
fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse {
return appleApiManager.fetchAppleOauthTokens(authorizationCode).also {
it.refreshToken ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
}
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt around
lines 15 to 22, the current code checks if tokenResponse.refreshToken is null
and throws an exception if so, then returns tokenResponse. Simplify this by
using a single expression that returns tokenResponse only if refreshToken is not
null, otherwise throws the exception, removing the explicit null check and
making the code more concise.

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ class AuthTokenService(
return TokenPairResponse.of(accessToken, refreshTokenResponse.refreshToken)
}

fun validateAndGetUserIdFromRefreshToken(@Valid tokenRefreshRequest: TokenRefreshRequest): UserIdResponse {
fun validateAndGetUserIdFromRefreshToken(tokenRefreshRequest: TokenRefreshRequest): UserIdResponse {
refreshTokenService.validateRefreshToken(tokenRefreshRequest.validRefreshToken())
return refreshTokenService.getUserIdByToken(tokenRefreshRequest)
}

fun deleteTokenForReissue(@Valid tokenRefreshRequest: TokenRefreshRequest) {
fun deleteRefreshTokenForReissue(tokenRefreshRequest: TokenRefreshRequest) {
refreshTokenService.deleteRefreshTokenByToken(tokenRefreshRequest.validRefreshToken())
}

fun deleteTokenForSignOut(@Valid deleteTokenRequest: DeleteTokenRequest) {
fun deleteRefreshTokenForSignOutOrWithdraw(@Valid deleteTokenRequest: DeleteTokenRequest) {
refreshTokenService.deleteRefreshTokenByToken(deleteTokenRequest.validRefreshToken())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import org.yapp.apis.auth.dto.request.TokenGenerateRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.response.RefreshTokenResponse
import org.yapp.apis.auth.dto.response.UserIdResponse
import org.yapp.domain.token.TokenDomainRedisService
import org.yapp.domain.token.RefreshTokenDomainService
import java.util.*

@Service
@Validated
class RefreshTokenService(
private val tokenDomainRedisService: TokenDomainRedisService,
private val refreshTokenDomainService: RefreshTokenDomainService,
) {
fun deleteRefreshTokenByToken(token: String) {
tokenDomainRedisService.deleteRefreshTokenByToken(token)
refreshTokenDomainService.deleteRefreshTokenByToken(token)
}

fun saveRefreshToken(@Valid tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse {
val token = tokenDomainRedisService.saveRefreshToken(
val token = refreshTokenDomainService.saveRefreshToken(
tokenGenerateRequest.validUserId(),
tokenGenerateRequest.validRefreshToken(),
tokenGenerateRequest.validExpiration()
Expand All @@ -29,16 +29,16 @@ class RefreshTokenService(
}

fun getRefreshTokenByUserId(userId: UUID): RefreshTokenResponse {
val token = tokenDomainRedisService.getRefreshTokenByUserId(userId)
val token = refreshTokenDomainService.getRefreshTokenByUserId(userId)
return RefreshTokenResponse.from(token)
}

fun validateRefreshToken(refreshToken: String) {
tokenDomainRedisService.validateRefreshTokenByToken(refreshToken)
refreshTokenDomainService.validateRefreshTokenByToken(refreshToken)
}

fun getUserIdByToken(@Valid tokenRefreshRequest: TokenRefreshRequest): UserIdResponse {
val userId = tokenDomainRedisService.getUserIdByToken(tokenRefreshRequest.validRefreshToken())
val userId = refreshTokenDomainService.getUserIdByToken(tokenRefreshRequest.validRefreshToken())
return UserIdResponse.from(userId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.yapp.apis.auth.service

import jakarta.validation.Valid
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.yapp.apis.user.dto.request.FindOrCreateUserRequest
import org.yapp.apis.user.dto.response.CreateUserResponse
import org.yapp.apis.user.service.UserAccountService

@Service
class UserSignInService(
private val userAccountService: UserAccountService,
) {
@Transactional
fun processSignIn(
@Valid request: FindOrCreateUserRequest,
appleRefreshToken: String?
): CreateUserResponse {
val createUserResponse = userAccountService.findOrCreateUser(request)

appleRefreshToken?.let {
userAccountService.updateAppleRefreshToken(createUserResponse.id, it)
}

return createUserResponse
}
}
Loading