Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,22 @@ data class WithdrawStrategyRequest private constructor(
example = "r_abc123def456ghi789jkl0mnopqrstu",
required = false
)
val appleRefreshToken: String?
val appleRefreshToken: String?,
@field:Schema(
description = "Google ๋กœ๊ทธ์ธ ์‹œ ๋ฐœ๊ธ‰๋ฐ›์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ (Google ๋กœ๊ทธ์ธ ํšŒ์› ํƒˆํ‡ด ์‹œ์—๋งŒ ํ•„์š”)",
example = "1//0g_xxxxxxxxxxxxxxxxxxxxxx",
required = false
)
val googleRefreshToken: String?
) {
companion object {
fun from(response: WithdrawTargetUserResponse): WithdrawStrategyRequest {
return WithdrawStrategyRequest(
userId = response.id,
providerType = response.providerType,
providerId = response.providerId,
appleRefreshToken = response.appleRefreshToken
appleRefreshToken = response.appleRefreshToken,
googleRefreshToken = response.googleRefreshToken
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum class AuthErrorCode(
PROVIDER_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_400_11", "์š”์ฒญ๋œ ๊ณต๊ธ‰์ž ํƒ€์ž…์ด ์‹ค์ œ ์‚ฌ์šฉ์ž์˜ ๊ณต๊ธ‰์ž ํƒ€์ž…๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
APPLE_REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, "AUTH_400_12", "Apple ์‚ฌ์šฉ์ž ํƒˆํ‡ด ์‹œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."),
KAKAO_UNLINK_FAILED(HttpStatus.BAD_REQUEST, "AUTH_400_15", "์นด์นด์˜ค ํšŒ์›ํƒˆํ‡ด ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."),
GOOGLE_REFRESH_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_400_16", "Google ์‚ฌ์šฉ์ž ํƒˆํ‡ด ์‹œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."),

/* 401 UNAUTHORIZED */
INVALID_OAUTH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401_01", "์ž˜๋ชป๋œ ์†Œ์…œ OAuth ํ† ํฐ์ž…๋‹ˆ๋‹ค."),
Expand Down Expand Up @@ -52,7 +53,8 @@ enum class AuthErrorCode(
"AUTH_500_06",
"Apple์—์„œ ์ดˆ๊ธฐ ๋กœ๊ทธ์ธ ์‹œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ์ œ๊ณตํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
),
KAKAO_UNLINK_RESPONSE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_08", "์นด์นด์˜ค ํšŒ์›ํƒˆํ‡ด ์‘๋‹ต์ด ์š”์ฒญ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
KAKAO_UNLINK_RESPONSE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_08", "์นด์นด์˜ค ํšŒ์›ํƒˆํ‡ด ์‘๋‹ต์ด ์š”์ฒญ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
FAILED_TO_REVOKE_TOKEN(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_09", "ํ† ํฐ ์ฒ ํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");

override fun getHttpStatus(): HttpStatus = httpStatus
override fun getCode(): String = code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.yapp.apis.auth.exception.AuthErrorCode
import org.yapp.apis.auth.exception.AuthException
import org.yapp.apis.config.GoogleOauthProperties
import org.yapp.infra.external.oauth.google.GoogleApi
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo

@Component
class GoogleApiManager(
Expand All @@ -16,7 +16,7 @@ class GoogleApiManager(
) {
private val log = KotlinLogging.logger {}

fun getUserInfo(accessToken: String): GoogleUserInfo {
fun getUserInfo(accessToken: String): GoogleUserInfo { // Changed to GoogleUserInfo
return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo)
.onSuccess { userInfo ->
log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" }
Expand All @@ -37,4 +37,9 @@ class GoogleApiManager(
}
}
}
}

fun revokeToken(token: String) {
googleApi.revokeGoogleToken(token)
.getOrThrow()
}
Comment on lines +41 to +44
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

revokeToken์— ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ๋กœ๊น…์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

getUserInfo(Lines 19-38)๋Š” ์„ฑ๊ณต/์‹คํŒจ ์‹œ ๋กœ๊น…๊ณผ ์˜ˆ์™ธ๋ฅผ AuthException์œผ๋กœ ๋ž˜ํ•‘ํ•˜๋Š” ๊ตฌ์กฐํ™”๋œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ์žˆ์ง€๋งŒ, revokeToken์€ getOrThrow()๋งŒ ํ˜ธ์ถœํ•˜์—ฌ ์›์‹œ ์˜ˆ์™ธ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ „ํŒŒ๋ฉ๋‹ˆ๋‹ค. ํ† ํฐ revoke ์‹คํŒจ ์‹œ ๋””๋ฒ„๊น…์ด ์–ด๋ ต๊ณ , AuthErrorCode.FAILED_TO_REVOKE_TOKEN์ด ์ •์˜๋˜์–ด ์žˆ์Œ์—๋„ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ getUserInfo์™€ ๋™์ผํ•œ ํŒจํ„ด์˜ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ ์šฉ ์ œ์•ˆ
     fun revokeToken(token: String) {
         googleApi.revokeGoogleToken(token)
-            .getOrThrow()
+            .onSuccess {
+                log.info { "Successfully revoked Google token" }
+            }
+            .getOrElse { exception ->
+                log.error(exception) { "Failed to revoke Google token" }
+                throw AuthException(
+                    AuthErrorCode.FAILED_TO_REVOKE_TOKEN,
+                    "Failed to revoke Google 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 revokeToken(token: String) {
googleApi.revokeGoogleToken(token)
.getOrThrow()
}
fun revokeToken(token: String) {
googleApi.revokeGoogleToken(token)
.onSuccess {
log.info { "Successfully revoked Google token" }
}
.getOrElse { exception ->
log.error(exception) { "Failed to revoke Google token" }
throw AuthException(
AuthErrorCode.FAILED_TO_REVOKE_TOKEN,
"Failed to revoke Google token.",
)
}
}
๐Ÿค– Prompt for AI Agents
In `@apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt` around
lines 41 - 44, revokeToken currently calls
googleApi.revokeGoogleToken(token).getOrThrow() without logging or wrapping
errors; change it to mirror getUserInfo: wrap the call in a try/catch, call
googleApi.revokeGoogleToken(token).getOrThrow() inside the try, and on Exception
log an error via the class logger including contextual info (mask the token) and
throw a new AuthException(AuthErrorCode.FAILED_TO_REVOKE_TOKEN, "Failed to
revoke token", e); reference revokeToken and googleApi.revokeGoogleToken and use
AuthException and AuthErrorCode.FAILED_TO_REVOKE_TOKEN for the wrapped error.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.yapp.apis.auth.strategy.withdraw

import mu.KotlinLogging
import org.springframework.stereotype.Component
import org.yapp.apis.auth.dto.request.WithdrawStrategyRequest
import org.yapp.apis.auth.exception.AuthErrorCode
import org.yapp.apis.auth.exception.AuthException
import org.yapp.apis.auth.manager.GoogleApiManager
import org.yapp.domain.user.ProviderType

@Component
class GoogleWithdrawStrategy(
private val googleApiManager: GoogleApiManager
) : WithdrawStrategy {

private val log = KotlinLogging.logger {}

override fun getProviderType(): ProviderType = ProviderType.GOOGLE

override fun withdraw(request: WithdrawStrategyRequest) {
val googleRefreshToken = request.googleRefreshToken
?: throw AuthException(AuthErrorCode.GOOGLE_REFRESH_TOKEN_NOT_FOUND, "Google Refresh Token์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")

try {
googleApiManager.revokeToken(googleRefreshToken as String)
Copy link

Choose a reason for hiding this comment

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

๐Ÿงน Nitpick | ๐Ÿ”ต Trivial

๋ถˆํ•„์š”ํ•œ ํƒ€์ž… ์บ์ŠคํŠธ (as String) ์ œ๊ฑฐ ํ•„์š”.

Line 21-22์—์„œ ?: throw๋ฅผ ํ†ตํ•ด null ์ฒดํฌ ํ›„ Kotlin ์Šค๋งˆํŠธ ์บ์ŠคํŠธ์— ์˜ํ•ด googleRefreshToken์€ ์ด๋ฏธ String ํƒ€์ž…์œผ๋กœ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค. as String ์บ์ŠคํŠธ๋Š” ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ
-            googleApiManager.revokeToken(googleRefreshToken as String)
+            googleApiManager.revokeToken(googleRefreshToken)
๐Ÿ“ 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
googleApiManager.revokeToken(googleRefreshToken as String)
googleApiManager.revokeToken(googleRefreshToken)
๐Ÿค– Prompt for AI Agents
In
`@apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt`
at line 25, Remove the unnecessary type cast on googleRefreshToken when calling
googleApiManager.revokeToken in GoogleWithdrawStrategy.kt: since you already
null-checked googleRefreshToken with the `?: throw` expression, rely on Kotlin's
smart cast and pass googleRefreshToken directly to revokeToken (remove `as
String`) to clean up the code and avoid redundant casting.

log.info { "Google refresh token revoked successfully for user ${request.userId}" }
} catch (e: Exception) {
log.error("Failed to revoke Google token for user ${request.userId}", e)
throw AuthException(AuthErrorCode.FAILED_TO_REVOKE_TOKEN, e.message)
}
Comment on lines +27 to +30
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Find AuthException class definition
find . -type f -name "*.kt" | xargs grep -l "class AuthException" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 135


๐Ÿ Script executed:

#!/bin/bash
# Get the AuthException class definition with details
rg "class AuthException" -A 20 --type=kt

Repository: YAPP-Github/Reed-Server

Length of output: 93


๐Ÿ Script executed:

#!/bin/bash
# Look at the GoogleWithdrawStrategy file to understand the context
find . -type f -name "GoogleWithdrawStrategy.kt" -exec cat -n {} \;

Repository: YAPP-Github/Reed-Server

Length of output: 1536


๐Ÿ Script executed:

#!/bin/bash
# Read the AuthException file content
cat -n ./apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthException.kt

Repository: YAPP-Github/Reed-Server

Length of output: 334


๐Ÿ Script executed:

#!/bin/bash
# Find CommonException class
find . -type f -name "*.kt" | xargs grep -l "class CommonException"

Repository: YAPP-Github/Reed-Server

Length of output: 147


๐Ÿ Script executed:

#!/bin/bash
# Read CommonException definition
find . -path "*globalutils/exception/CommonException.kt" -exec cat -n {} \;

Repository: YAPP-Github/Reed-Server

Length of output: 436


๐Ÿ Script executed:

#!/bin/bash
# Find GoogleApiManager
find . -type f -name "*GoogleApiManager*" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 136


๐Ÿ Script executed:

#!/bin/bash
# Look at GoogleApiManager.revokeToken method
find . -type f -name "*.kt" | xargs grep -l "GoogleApiManager" | head -3

Repository: YAPP-Github/Reed-Server

Length of output: 222


๐Ÿ Script executed:

#!/bin/bash
# Read GoogleApiManager file
cat -n ./apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt

Repository: YAPP-Github/Reed-Server

Length of output: 2074


๐Ÿ Script executed:

#!/bin/bash
# Check if ResponseStatusException supports cause parameter
find . -type f -name "*.kt" | xargs grep -l "ResponseStatusException" | head -2

Repository: YAPP-Github/Reed-Server

Length of output: 147


๐Ÿ Script executed:

#!/bin/bash
# Check what GoogleApi.revokeGoogleToken throws
find . -type f -name "GoogleApi.kt" | xargs grep -A 10 "revokeGoogleToken"

Repository: YAPP-Github/Reed-Server

Length of output: 344


๐Ÿ Script executed:

#!/bin/bash
# Check ResponseStatusException to understand what parameters it accepts
# Look for how exceptions are constructed in similar patterns
find . -type f -name "*.kt" | xargs grep -B2 -A2 "throw AuthException" | head -30

Repository: YAPP-Github/Reed-Server

Length of output: 3445


๐Ÿ Script executed:

#!/bin/bash
# Check if there are other exception handling patterns in the codebase with cause
find . -type f -name "*.kt" | xargs grep "Throwable\|cause" | grep -i "exception\|error" | head -10

Repository: YAPP-Github/Reed-Server

Length of output: 49


๐Ÿ Script executed:

#!/bin/bash
# Check Spring ResponseStatusException constructor to understand cause support
python3 << 'EOF'
# ResponseStatusException is from Spring framework
# Let's check how it's typically used in Kotlin/Spring context
# and what the standard exception constructor signature looks like

print("Spring's ResponseStatusException class signature:")
print("public ResponseStatusException(HttpStatus status, `@Nullable` String reason, `@Nullable` Throwable cause)")
print("\nHowever, the current CommonException implementation doesn't pass cause to parent.")
EOF

Repository: YAPP-Github/Reed-Server

Length of output: 301


๐Ÿ Script executed:

#!/bin/bash
# Check if there are any other places catching RestClientException or similar
find . -type f -name "*.kt" | xargs grep -l "RestClientException\|HttpClientErrorException" | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 205


๐Ÿ Script executed:

#!/bin/bash
# Check KakaoApiManager to see similar exception handling patterns
cat -n ./apis/src/main/kotlin/org/yapp/apis/auth/manager/KakaoApiManager.kt | head -60

Repository: YAPP-Github/Reed-Server

Length of output: 3012


๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž… ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์ฝ”๋“œ์˜ ๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

  1. Exception์„ ํฌ๊ด„์ ์œผ๋กœ ์žก๋Š” ๊ฒƒ์€ ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ํŒจํ‚ค์ง€์˜ GoogleApiManager์™€ KakaoApiManager์—์„œ ๋ณด๋“ฏ์ด, RestClientException ๋˜๋Š” HttpClientErrorException ๊ฐ™์€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์™ธ ํƒ€์ž…์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  2. e.message๋งŒ ์ „๋‹ฌํ•˜๋ฉด ์›๋ณธ ์˜ˆ์™ธ์˜ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ์œ ์‹ค๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ํ˜„์žฌ AuthException์€ cause ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ด๋ฅผ ๊ฐœ์„ ํ•˜๋ ค๋ฉด ๋จผ์ € AuthException๊ณผ ๋ถ€๋ชจ ํด๋ž˜์Šค CommonException์ด cause ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์›ํ•˜๋„๋ก ์ˆ˜์ •๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ› ๊ตฌ์ฒด์  ์˜ˆ์™ธ ํƒ€์ž… ์ฒ˜๋ฆฌ ์ œ์•ˆ
-        } catch (e: Exception) {
+        } catch (e: org.springframework.web.client.RestClientException) {
             log.error("Failed to revoke Google token for user ${request.userId}", e)
             throw AuthException(AuthErrorCode.FAILED_TO_REVOKE_TOKEN, e.message)
         }
๐Ÿงฐ Tools
๐Ÿช› detekt (1.23.8)

[warning] 27-27: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)


[warning] 27-27: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

๐Ÿค– Prompt for AI Agents
In
`@apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/GoogleWithdrawStrategy.kt`
around lines 27 - 30, The current catch-all in GoogleWithdrawStrategy.kt
(catching Exception) should be replaced with specific exception handlers (e.g.,
catch RestClientException and HttpClientErrorException as seen in
GoogleApiManager/KakaoApiManager) so only expected HTTP/client errors are
handled; also update AuthException and its parent CommonException to accept and
store a cause parameter so you can throw
AuthException(AuthErrorCode.FAILED_TO_REVOKE_TOKEN, e.message, e) (preserving
the original stack trace) instead of passing only e.message, and ensure the new
constructors are used in GoogleWithdrawStrategy's error handling.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "oauth.google")
data class GoogleOauthProperties(
val url: Url,
val clientId: String
val clientId: String,
val clientSecret: String? = null,
val grantType: String? = null,
val redirectUri: String? = null
)

data class Url(
val userInfo: String
)
val userInfo: String,
val tokenUri: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ data class WithdrawTargetUserResponse private constructor(
val providerId: String,

@field:Schema(description = "Apple Refresh Token (์• ํ”Œ ํšŒ์› ํƒˆํ‡ด ์‹œ ํ•„์š”, ์นด์นด์˜ค๋Š” null)")
val appleRefreshToken: String? = null
val appleRefreshToken: String? = null,
@field:Schema(description = "Google Refresh Token (๊ตฌ๊ธ€ ํšŒ์› ํƒˆํ‡ด ์‹œ ํ•„์š”, ์นด์นด์˜ค/์• ํ”Œ์€ null)")
val googleRefreshToken: String? = null
) {
companion object {
fun from(vo: WithdrawTargetUserVO): WithdrawTargetUserResponse {
return WithdrawTargetUserResponse(
id = vo.id.value,
providerType = vo.providerType,
providerId = vo.providerId.value,
appleRefreshToken = vo.appleRefreshToken
appleRefreshToken = vo.appleRefreshToken,
googleRefreshToken = vo.googleRefreshToken
)
}
}
Expand Down
10 changes: 7 additions & 3 deletions apis/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ swagger:
oauth:
google:
url:
user-info: https://www.googleapis.com/oauth2/v2/userinfo
user-info: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
client-id: ${GOOGLE_CLIENT_ID}

---
Expand Down Expand Up @@ -95,5 +95,9 @@ springdoc:
oauth:
google:
url:
user-info: https://www.googleapis.com/oauth2/v2/userinfo
client-id: test-client-id
user-info: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
token-uri: ${GOOGLE_TOKEN_URI:test-token-uri}
client-id: ${GOOGLE_CLIENT_ID:test-client-id}
client-secret: ${GOOGLE_CLIENT_SECRET:test-client-secret}
grant-type: ${GOOGLE_GRANT_TYPE:test-grant-type}
redirect-uri: ${GOOGLE_REDIRECT_URI:test-redirect-uri}
12 changes: 12 additions & 0 deletions domain/src/main/kotlin/org/yapp/domain/user/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ data class User private constructor(
val role: Role,
val termsAgreed: Boolean = false,
val appleRefreshToken: String? = null,
val googleRefreshToken: String? = null,
val notificationEnabled: Boolean = true,
val lastActivity: LocalDateTime? = null,
val createdAt: LocalDateTime? = null,
Expand All @@ -42,6 +43,12 @@ data class User private constructor(
)
}

fun updateGoogleRefreshToken(token: String): User {
return this.copy(
googleRefreshToken = token
)
}

fun updateNotificationEnabled(enabled: Boolean): User {
return this.copy(
notificationEnabled = enabled
Expand Down Expand Up @@ -80,6 +87,7 @@ data class User private constructor(
role = Role.USER,
termsAgreed = termsAgreed,
appleRefreshToken = null,
googleRefreshToken = null,
notificationEnabled = notificationEnabled,
lastActivity = LocalDateTime.now()
)
Expand All @@ -106,6 +114,7 @@ data class User private constructor(
role = role,
termsAgreed = termsAgreed,
appleRefreshToken = null,
googleRefreshToken = null,
notificationEnabled = notificationEnabled,
lastActivity = LocalDateTime.now()
)
Expand All @@ -121,6 +130,7 @@ data class User private constructor(
role: Role,
termsAgreed: Boolean = false,
appleRefreshToken: String? = null,
googleRefreshToken: String? = null,
notificationEnabled: Boolean = true,
lastActivity: LocalDateTime? = null,
createdAt: LocalDateTime? = null,
Expand All @@ -137,6 +147,7 @@ data class User private constructor(
role = role,
termsAgreed = termsAgreed,
appleRefreshToken = appleRefreshToken,
googleRefreshToken = googleRefreshToken,
notificationEnabled = notificationEnabled,
lastActivity = lastActivity,
createdAt = createdAt,
Expand Down Expand Up @@ -175,3 +186,4 @@ data class User private constructor(
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ class UserDomainService(
return UserAuthVO.newInstance(updatedUser)
}

fun updateGoogleRefreshToken(userId: UUID, refreshToken: String): UserAuthVO {
val user = userRepository.findById(userId)
?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND)

val updatedUser = userRepository.save(user.updateGoogleRefreshToken(refreshToken))

return UserAuthVO.newInstance(updatedUser)
}

fun deleteUser(userId: UUID) {
val user = userRepository.findById(userId)
?: throw UserNotFoundException(UserErrorCode.USER_NOT_FOUND)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ data class WithdrawTargetUserVO private constructor(
val id: User.Id,
val providerType: ProviderType,
val providerId: User.ProviderId,
val appleRefreshToken: String?
val appleRefreshToken: String?,
val googleRefreshToken: String?
) {
companion object {
fun newInstance(user: User): WithdrawTargetUserVO {
return WithdrawTargetUserVO(
id = user.id,
providerType = user.providerType,
providerId = user.providerId,
appleRefreshToken = user.appleRefreshToken
appleRefreshToken = user.appleRefreshToken,
googleRefreshToken = user.googleRefreshToken
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
package org.yapp.infra.external.oauth.google

import org.springframework.stereotype.Component
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo
import org.springframework.util.LinkedMultiValueMap
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo
Copy link

Choose a reason for hiding this comment

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

๐Ÿงน Nitpick | ๐Ÿ”ต Trivial

๋ฆฌํŒฉํ† ๋ง ์ž”์—ฌ ์ฃผ์„ ์ œ๊ฑฐ ํ•„์š”.

// Changed to GoogleUserInfo ์ฃผ์„์€ ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์˜ ๋ฉ”๋ชจ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ ํžˆ์Šคํ† ๋ฆฌ๋Š” Git์—์„œ ๊ด€๋ฆฌ๋˜๋ฏ€๋กœ ์ œ๊ฑฐํ•ด ์ฃผ์„ธ์š”. Line 19์—๋„ ๋™์ผํ•œ ์ฃผ์„์ด ์žˆ์Šต๋‹ˆ๋‹ค.

โ™ป๏ธ ์ œ์•ˆ
-import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo
+import org.yapp.infra.external.oauth.google.response.GoogleUserInfo

Line 19๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ:

-    ): Result<GoogleUserInfo> { // Changed to GoogleUserInfo
+    ): Result<GoogleUserInfo> {
๐Ÿ“ 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
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo // Changed to GoogleUserInfo
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo
๐Ÿค– Prompt for AI Agents
In `@infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt` at
line 5, Remove leftover refactor comments that are just notes: delete the
trailing "// Changed to GoogleUserInfo" from the import line that references
GoogleUserInfo (the import of
org.yapp.infra.external.oauth.google.response.GoogleUserInfo) and remove the
identical comment at line 19 in GoogleApi.kt so only the clean import and code
remain.


@Component
class GoogleApi(
private val googleRestClient: GoogleRestClient
) {
companion object {
private const val BEARER_PREFIX = "Bearer "
private const val TOKEN = "token"
}

fun fetchUserInfo(
accessToken: String,
userInfoUrl: String,
): Result<GoogleUserInfo> {
): Result<GoogleUserInfo> { // Changed to GoogleUserInfo
return runCatching {
googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl)
}
}

fun revokeGoogleToken(
token: String
): Result<Unit> {
val requestBody = LinkedMultiValueMap<String, String>().apply {
add(TOKEN, token)
}

return runCatching {
googleRestClient.revoke(requestBody)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yapp.infra.external.oauth.google

import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.util.LinkedMultiValueMap
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo

@Component
Expand All @@ -21,4 +22,12 @@ class GoogleRestClient(
.body(GoogleUserInfo::class.java)
?: throw IllegalStateException("Google API ์‘๋‹ต์ด null ์ž…๋‹ˆ๋‹ค.")
}

fun revoke(requestBody: LinkedMultiValueMap<String, String>) {
client.post()
.uri("https://oauth2.googleapis.com/revoke")
.body(requestBody)
.retrieve()
.body(Unit::class.java) // Google revoke API typically returns an empty body
}
Comment on lines +26 to +32
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐ŸŒ Web query:

Spring RestClient toBodilessEntity vs body Unit deserialization empty response

๐Ÿ’ก Result:

In Springโ€™s RestClient, these two calls do different things:

  • retrieve().toBodilessEntity() does not try to read/convert the response body at all; it just returns status + headers with a ResponseEntity<Void>. Spring explicitly recommends it โ€œif consuming the response has no interest for your use caseโ€. [1][2]
  • retrieve().body<T>() does try to decode a body using HttpMessageConverters (e.g., Jackson for application/json). If the server replies with Content-Type: application/json but sends an empty body, Jackson can throw HttpMessageNotReadableException: No content to map due to end-of-input (and Spring wraps it), instead of returning nullโ€”this mismatch is documented in Spring Framework issue #31719. [3][2]

What to do for Kotlin Unit + empty response

If the endpoint legitimately returns no body (common for 204/201/202), donโ€™t deserialize into Unit:

restClient.post()
  .uri("/...")
  .body(request)
  .retrieve()
  .toBodilessEntity()

If you need status/headers too, this is still the right choice. [1][2]

If you must use body<T>()

Then the server should return either:

  • 204 No Content (no Content-Type, no body), or
  • a real JSON payload (even null as the literal body "null"), because an โ€œempty body + application/jsonโ€ is what triggers the Jackson โ€œend-of-inputโ€ failure described in #31719. [3]

Sources: Spring REST client reference docs and RestClient.ResponseSpec Javadoc for toBodilessEntity()/body(), plus Spring issue #31719 describing the empty-body deserialization exception. [1][2][3]

[1] https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
[2] https://docs.spring.io/spring-framework/docs/current-SNAPSHOT/javadoc-api/org/springframework/web/client/RestClient.ResponseSpec.html
[3] spring-projects/spring-framework#31719


๐Ÿ Script executed:

# Find and examine the GoogleRestClient.kt file
fd -t f "GoogleRestClient.kt" -x cat -n {}

Repository: YAPP-Github/Reed-Server

Length of output: 1315


๐Ÿ Script executed:

# Check surrounding code and other methods in GoogleRestClient
fd -t f "GoogleRestClient.kt" -x sh -c 'wc -l {} && cat -n {}'

Repository: YAPP-Github/Reed-Server

Length of output: 1399


๐Ÿ Script executed:

# Look for configuration patterns and how URLs are managed in the project
fd -t f "application.yml" -o "application.yaml" | head -5 | xargs cat

Repository: YAPP-Github/Reed-Server

Length of output: 218


๐Ÿ Script executed:

# Search for getUserInfo method to understand the parameterized URL pattern
rg -A 5 "fun getUserInfo" --type kotlin

Repository: YAPP-Github/Reed-Server

Length of output: 2859


body(Unit::class.java) ์‚ฌ์šฉ ์‹œ ์—ญ์ง๋ ฌํ™” ์‹คํŒจ ๋ฐ URL ํ•˜๋“œ์ฝ”๋”ฉ ๋ฌธ์ œ

๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. body(Unit::class.java) ์‚ฌ์šฉ ๋ถˆ๊ฐ€: Google revoke API๊ฐ€ ๋นˆ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ, Spring RestClient๊ฐ€ ์‘๋‹ต์„ ์—ญ์ง๋ ฌํ™”ํ•˜๋ ค๋‹ค ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ๋นˆ ์‘๋‹ต ๋ณธ๋ฌธ์— ๋Œ€ํ•ด HttpMessageConverter๊ฐ€ ์—†์–ด HttpMessageNotReadableException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์‘๋‹ต ๋ณธ๋ฌธ์ด ํ•„์š” ์—†์œผ๋ฏ€๋กœ toBodilessEntity()๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

  2. URL ํ•˜๋“œ์ฝ”๋”ฉ ๋ฌธ์ œ: getUserInfo๋Š” URL์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์œผ๋‚˜, revoke URL์€ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ์„ค์ • ํŒจํ„ด(googleOauthProperties.url)์„ ๋”ฐ๋ผ configurableํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•˜์„ธ์š”.

์ˆ˜์ • ์ œ์•ˆ
     fun revoke(requestBody: LinkedMultiValueMap<String, String>) {
         client.post()
             .uri("https://oauth2.googleapis.com/revoke")
             .body(requestBody)
             .retrieve()
-            .body(Unit::class.java) // Google revoke API typically returns an empty body
+            .toBodilessEntity()
     }
๐Ÿค– Prompt for AI Agents
In
`@infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt`
around lines 26 - 32, The revoke method in GoogleRestClient is using
body(Unit::class.java) which causes deserialization errors for an empty response
and it hardcodes the revoke URL; change revoke to call toBodilessEntity() (or
retrieve().toBodilessEntity()) so no message conversion is attempted, and obtain
the revoke endpoint from the existing googleOauthProperties.url pattern (or a
dedicated property on GoogleOauthProperties) instead of the hardcoded
"https://oauth2.googleapis.com/revoke"; update references in revoke and keep
consistency with getUserInfo.

}