Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,6 +5,7 @@ import jakarta.validation.constraints.NotBlank
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.GoogleAuthCredentials
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

Google λΆ„κΈ° λ‘œμ§μ€ κΉ”λ”ν•˜μ§€λ§Œ, 토큰 의미(id_token vs access_token)λ₯Ό λͺ…ν™•νžˆ ν•΄ λ‘λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€

ProviderType.GOOGLE -> GoogleAuthCredentials(request.validOauthToken()) 둜 Kakao 와 λ™μΌν•œ νŒ¨ν„΄μœΌλ‘œ 뢙인 점은 읽기 μ’‹μŠ΅λ‹ˆλ‹€.
λ‹€λ§Œ ν”„λ‘ νŠΈμ—μ„œ ν˜„μž¬ id_token 을 λ³΄λ‚΄λŠ” ꡬ쑰라면, μ—¬κΈ°μ„œ μƒμ„±λ˜λŠ” GoogleAuthCredentials.accessToken 은 μ‹€μ œλ‘œλŠ” id_token 을 κ°€λ¦¬ν‚€κ²Œ 되고, 이후 GoogleApiManager μ—μ„œλŠ” 이λ₯Ό UserInfo 호좜용 access token 으둜 μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€.

μ•ž/뒀단 λͺ¨λ‘μ—μ„œ μ–΄λ–€ μ’…λ₯˜μ˜ 토큰을 μ£Όκ³ λ°›λŠ”μ§€(access_token / id_token)λ₯Ό μ£Όμ„μ΄λ‚˜ λ¬Έμ„œ, ν˜Ήμ€ ν•„λ“œλͺ… λ³€κ²½μœΌλ‘œ λͺ…ν™•νžˆ ν•΄ 두면, 이후 μœ μ§€λ³΄μˆ˜ μ‹œ μ˜€ν•΄λ‚˜ 버그λ₯Ό 쀄일 수 μžˆμ„ 것 κ°™μŠ΅λ‹ˆλ‹€.

Also applies to: 55-67

πŸ€– Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt
around lines 8 and 55-67, the code uses
GoogleAuthCredentials(request.validOauthToken()) without clarifying whether the
incoming token is an id_token or an access_token; update the DTO and usage to
make the token type explicit: annotate the SocialLoginRequest field with a
comment stating which token is expected, or better rename the parameter/field to
idToken if the frontend sends id_token, add a matching constructor or factory
for GoogleAuthCredentials that accepts an idToken (or separate properties
idToken/accessToken), and update GoogleApiManager calls to use the correct token
property (idToken for verification / JWT parsing, accessToken for userinfo
calls) so the intent is clear and prevents future misuse.

import org.yapp.apis.auth.strategy.signin.KakaoAuthCredentials
import org.yapp.apis.auth.strategy.signin.SignInCredentials
import org.yapp.domain.user.ProviderType
Expand Down Expand Up @@ -61,6 +62,8 @@ data class SocialLoginRequest private constructor(
)
AppleAuthCredentials(request.validOauthToken(), authCode)
}

ProviderType.GOOGLE -> GoogleAuthCredentials(request.validOauthToken())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.yapp.apis.auth.manager

import mu.KotlinLogging
import org.springframework.stereotype.Component
import org.springframework.web.client.HttpClientErrorException
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

@Component
class GoogleApiManager(
private val googleApi: GoogleApi,
private val googleOauthProperties: GoogleOauthProperties,
) {
private val log = KotlinLogging.logger {}

fun getUserInfo(accessToken: String): 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

νŒŒλΌλ―Έν„°μ— λŒ€ν•œ λ¬Έμ„œν™”λ₯Ό μΆ”κ°€ν•˜λŠ” 것을 κ³ λ €ν•˜μ„Έμš”.

이전 λ¦¬λ·°μ—μ„œ 제기된 access_token vs id_token ꡬ뢄에 λŒ€ν•œ λͺ…ν™•μ„± λ¬Έμ œμ™€ κ΄€λ ¨ν•˜μ—¬, νŒŒλΌλ―Έν„° 이름은 λͺ…ν™•ν•˜μ§€λ§Œ KDoc 주석을 μΆ”κ°€ν•˜λ©΄ 더 μ’‹μŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 λ¬Έμ„œν™”λ₯Ό μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

+/**
+ * Google OAuth access token을 μ‚¬μš©ν•˜μ—¬ μ‚¬μš©μž 정보λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.
+ * @param accessToken Google OAuth 2.0 access token (id_token이 μ•„λ‹˜)
+ * @return 쑰회된 Google μ‚¬μš©μž 정보
+ * @throws AuthException 토큰이 μœ νš¨ν•˜μ§€ μ•Šκ±°λ‚˜ Google μ„œλ²„μ™€ 톡신 μ‹€νŒ¨ μ‹œ
+ */
 fun getUserInfo(accessToken: String): GoogleUserInfo {
πŸ€– Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/manager/GoogleApiManager.kt around
line 19, the public function parameter lacks KDoc explaining whether the token
is an OAuth access_token or an id_token and what format/value is expected; add a
KDoc block for the function that documents the purpose of the function,
clarifies that the parameter accessToken is an OAuth access_token (not an
id_token), describes expected format/expiration and any required scopes, and
briefly states what the returned GoogleUserInfo contains and possible
exceptions/errors thrown.

return googleApi.fetchUserInfo(accessToken, googleOauthProperties.url.userInfo)
.onSuccess { userInfo ->
log.info { "Successfully fetched Google user info for userId: ${userInfo.id}" }
}
.getOrElse { exception ->
log.error(exception) { "Failed to fetch Google user info" }

when (exception) {
is HttpClientErrorException -> throw AuthException(
AuthErrorCode.INVALID_OAUTH_TOKEN,
"Invalid Google Access Token.",
)

else -> throw AuthException(
AuthErrorCode.OAUTH_SERVER_ERROR,
"Failed to communicate with Google server.",
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.yapp.apis.auth.strategy.signin

import mu.KotlinLogging
import org.springframework.stereotype.Component
import org.yapp.apis.auth.dto.response.UserCreateInfoResponse
import org.yapp.apis.auth.exception.AuthErrorCode
import org.yapp.apis.auth.exception.AuthException
import org.yapp.apis.auth.manager.GoogleApiManager
import org.yapp.apis.auth.util.NicknameGenerator
import org.yapp.domain.user.ProviderType
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo

@Component
class GoogleSignInStrategy(
private val googleApiManager: GoogleApiManager
) : SignInStrategy {

private val log = KotlinLogging.logger {}

override fun getProviderType(): ProviderType = ProviderType.GOOGLE

override fun authenticate(credentials: SignInCredentials): UserCreateInfoResponse {
return try {
val googleCredentials = validateCredentials(credentials)
val googleUser = googleApiManager.getUserInfo(googleCredentials.accessToken)
createUserInfo(googleUser)
} catch (exception: Exception) {
log.error("Google authentication failed", exception)
when (exception) {
is AuthException -> throw exception
else -> throw AuthException(AuthErrorCode.FAILED_TO_GET_USER_INFO, exception.message)
}
}
}
Comment on lines +22 to +34
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

μ˜ˆμ™Έ 처리 λ²”μœ„ κ²€ν†  ꢌμž₯

ν˜„μž¬ Exception을 catchν•˜μ—¬ λͺ¨λ“  μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. λ‘œκΉ…κ³Ό 재throw λ‘œμ§μ€ μ μ ˆν•˜μ§€λ§Œ, Google API 호좜 μ‹œ λ°œμƒ κ°€λŠ₯ν•œ ꡬ체적인 μ˜ˆμ™Έ νƒ€μž…(예: HTTP ν΄λΌμ΄μ–ΈνŠΈ μ˜ˆμ™Έ, λ„€νŠΈμ›Œν¬ μ˜ˆμ™Έ λ“±)을 catchν•˜λ©΄ 더 λͺ…ν™•ν•œ μ—λŸ¬ 핸듀링이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

ν˜„μž¬ κ΅¬ν˜„λ„ 방어적이고 μ „λž΅ νŒ¨ν„΄ μ»¨ν…μŠ€νŠΈμ—μ„œ 수용 κ°€λŠ₯ν•˜μ§€λ§Œ, ν•„μš”μ‹œ ꡬ체적인 μ˜ˆμ™Έ νƒ€μž…μœΌλ‘œ μ„ΈλΆ„ν™”ν•˜λŠ” 것을 κ³ λ €ν•΄λ³΄μ„Έμš”.

정적 뢄석 λ„κ΅¬μ˜ κ²½κ³ λ₯Ό μ°Έκ³ ν–ˆμŠ΅λ‹ˆλ‹€.

🧰 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/signin/GoogleSignInStrategy.kt
around lines 22 to 34, the current code catches the broad Exception type; narrow
the exception handling by catching specific exceptions (e.g., AuthException
first, HTTP client exceptions such as HttpClientRequestException or Ktor's
ClientRequestException/ServerResponseException, IO/SocketTimeoutException, and
other network-related exceptions) and handle each appropriately: log the error,
rethrow AuthException as-is, and wrap other known exception types into
AuthException with the proper AuthErrorCode and original exception as cause;
keep a final generic catch only if necessary to preserve behavior but prefer
explicit catches to satisfy static analysis and provide clearer error semantics.


private fun validateCredentials(credentials: SignInCredentials): GoogleAuthCredentials {
return credentials as? GoogleAuthCredentials
?: throw AuthException(
AuthErrorCode.INVALID_CREDENTIALS,
"Credentials must be GoogleAuthCredentials"
)
}

private fun createUserInfo(googleUser: GoogleUserInfo): UserCreateInfoResponse {
return UserCreateInfoResponse.of(
email = googleUser.email ?: ("google_${googleUser.id}@google.com"),
nickname = NicknameGenerator.generate(),
profileImageUrl = googleUser.picture,
providerType = ProviderType.GOOGLE,
providerId = googleUser.id
)
}
Comment on lines +44 to +52
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "GoogleSignInStrategy.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 148


🏁 Script executed:

git ls-files | grep -i google | head -20

Repository: YAPP-Github/Reed-Server

Length of output: 381


🏁 Script executed:

git ls-files | grep -i auth | head -30

Repository: YAPP-Github/Reed-Server

Length of output: 2278


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserCreateInfoResponse.kt

Repository: YAPP-Github/Reed-Server

Length of output: 1651


🏁 Script executed:

cat infra/src/main/kotlin/org/yapp/infra/external/oauth/google/response/GoogleUserInfo.kt

Repository: YAPP-Github/Reed-Server

Length of output: 354


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt

Repository: YAPP-Github/Reed-Server

Length of output: 2142


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt

Repository: YAPP-Github/Reed-Server

Length of output: 1272


🏁 Script executed:

rg "email" --type kt apis/src/main/kotlin/org/yapp/apis/auth/ -B 2 -A 2 | head -80

Repository: YAPP-Github/Reed-Server

Length of output: 93


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/AppleSignInStrategy.kt

Repository: YAPP-Github/Reed-Server

Length of output: 1852


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/KakaoSignInStrategy.kt

Repository: YAPP-Github/Reed-Server

Length of output: 2136


🏁 Script executed:

rg "FindOrCreateUserRequest" --type kotlin -A 20

Repository: YAPP-Github/Reed-Server

Length of output: 16922


🏁 Script executed:

rg "validate.*email|email.*validate" --type kotlin -i -B 2 -A 5

Repository: YAPP-Github/Reed-Server

Length of output: 1144


🏁 Script executed:

rg "sendEmail|send.*mail|mailService" --type kotlin -i

Repository: YAPP-Github/Reed-Server

Length of output: 49


🏁 Script executed:

rg "class User" --type kotlin | grep -v "Response\|Request\|Info\|Strategy"

Repository: YAPP-Github/Reed-Server

Length of output: 2816


🏁 Script executed:

git log --oneline --all -- apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt | head -5

Repository: YAPP-Github/Reed-Server

Length of output: 129


🏁 Script executed:

rg "email" --type kotlin apis/src/main/kotlin/org/yapp/apis/user/ -B 2 -A 2 | head -100

Repository: YAPP-Github/Reed-Server

Length of output: 4728


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt

Repository: YAPP-Github/Reed-Server

Length of output: 2399


🏁 Script executed:

cat domain/src/main/kotlin/org/yapp/domain/user/User.kt

Repository: YAPP-Github/Reed-Server

Length of output: 5536


🏁 Script executed:

find . -type f -name "*Test.kt" | grep -i auth | head -10

Repository: YAPP-Github/Reed-Server

Length of output: 49


🏁 Script executed:

rg "getOrDefaultEmail" --type kotlin -B 5 -A 5

Repository: YAPP-Github/Reed-Server

Length of output: 2616


🏁 Script executed:

rg "EmailValidator" --type kotlin -B 3 -A 10

Repository: YAPP-Github/Reed-Server

Length of output: 3437


🏁 Script executed:

find . -type f \( -name "*Test.kt" -o -name "*Tests.kt" \) -exec grep -l "email\|signin\|sign-in" {} \; | head -10

Repository: YAPP-Github/Reed-Server

Length of output: 49


🏁 Script executed:

rg "isValidEmail" --type kotlin -B 5 -A 15

Repository: YAPP-Github/Reed-Server

Length of output: 2687


🏁 Script executed:

rg "EMAIL_PATTERN" --type kotlin -B 2 -A 2

Repository: YAPP-Github/Reed-Server

Length of output: 1070


🏁 Script executed:

find . -type f -name "RegexUtils.kt"

Repository: YAPP-Github/Reed-Server

Length of output: 137


🏁 Script executed:

cat global-utils/src/main/kotlin/org/yapp/globalutils/util/RegexUtils.kt

Repository: YAPP-Github/Reed-Server

Length of output: 423


🏁 Script executed:

rg "providerId.*unique\|unique.*providerId" --type kotlin -i

Repository: YAPP-Github/Reed-Server

Length of output: 49


🏁 Script executed:

rg "findUserByProviderTypeAndProviderId" --type kotlin -B 3 -A 10

Repository: YAPP-Github/Reed-Server

Length of output: 4431


🏁 Script executed:

cat apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt | grep -i "email\|user"

Repository: YAPP-Github/Reed-Server

Length of output: 413


Email fallback handling is working as designed; consider documenting the approach.

The synthetic email fallback is intentional and will not cause validation failures. The EmailValidator only performs regex validation (allows underscores and dots), with no MX record checks. Emails like [email protected] pass validation successfully.

However, there are design consistency issues worth addressing:

  1. Redundant fallback logic: GoogleSignInStrategy creates google_<id>@google.com, but FindOrCreateUserRequest.getOrDefaultEmail() independently creates <id>@google.local if email is null. This layering is unnecessary.

  2. Inconsistent formats: Google/Kakao use provider_<id>@provider.com, while FindOrCreateUserRequest uses <id>@provider.local. The .local convention is typically for local networks, not production systems.

  3. Design alignment: AppleSignInStrategy accepts nullable emails directly, while Google/Kakao generate fallbacksβ€”this variation should be intentional and documented.

The core design (allowing nullable emails with providerId as the true identifier) is sound. Consider:

  • Centralizing email fallback logic (remove from GoogleSignInStrategy, keep only in FindOrCreateUserRequest)
  • Using consistent fallback format across all providers
  • Adding a flag to identify synthetic emails (useful if email features are added later)
  • Documenting that providerId, not email, is the primary user identifier
πŸ€– Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/GoogleSignInStrategy.kt
around lines 44-52, remove the synthetic email fallback so createUserInfo
returns the raw googleUser.email (nullable) rather than generating
"google_<id>@google.com"; centralize fallback logic into
FindOrCreateUserRequest.getOrDefaultEmail() so all providers share one
consistent format, pick and apply a single fallback pattern (e.g.
provider_<id>@provider.local or provider_<id>@provider.com) in that central
method, add a boolean flag (e.g. isSyntheticEmail) to the request/response
objects and set it when the central fallback is used, and update related
callers, tests and documentation to reflect that providerId is the primary
identifier and synthetic email behavior is centralized and consistent.

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ sealed class SignInCredentials {
}

data class KakaoAuthCredentials(
val accessToken: String
val accessToken: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType = ProviderType.KAKAO
}

data class AppleAuthCredentials(
val idToken: String,
val authorizationCode: String
val authorizationCode: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType = ProviderType.APPLE
}

data class GoogleAuthCredentials(
val accessToken: String,
) : SignInCredentials() {
override fun getProviderType(): ProviderType {
return ProviderType.GOOGLE
}
Comment on lines +25 to +27
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

ν‘œν˜„μ‹ λ°”λ””λ‘œ ν†΅μΌν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€.

getProviderType() λ©”μ„œλ“œκ°€ 동일 파일 λ‚΄ λ‹€λ₯Έ κ΅¬ν˜„μ²΄(KakaoAuthCredentials, AppleAuthCredentials)μ—μ„œλŠ” ν‘œν˜„μ‹ λ°”λ””(= ProviderType.XXX)λ₯Ό μ‚¬μš©ν•˜λŠ” 반면, μ—¬κΈ°μ„œλ§Œ 블둝 λ°”λ””λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ–΄ μŠ€νƒ€μΌ 일관성이 λ–¨μ–΄μ§‘λ‹ˆλ‹€.

λ‹€μŒ diffλ₯Ό μ μš©ν•˜μ—¬ 일관성을 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

-    override fun getProviderType(): ProviderType {
-        return ProviderType.GOOGLE
-    }
+    override fun getProviderType(): ProviderType = ProviderType.GOOGLE
πŸ“ 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
override fun getProviderType(): ProviderType {
return ProviderType.GOOGLE
}
override fun getProviderType(): ProviderType = ProviderType.GOOGLE
πŸ€– Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt
around lines 25 to 27, the getProviderType() method uses a block body while
other credential implementations (KakaoAuthCredentials, AppleAuthCredentials)
use expression bodies; change the method from a block body to an expression body
so it reads as a single-line expression returning ProviderType.GOOGLE to match
project style and maintain consistency.

}

12 changes: 12 additions & 0 deletions apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.yapp.apis.config

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "oauth.google")
data class GoogleOauthProperties(
val url: Url
)

data class Url(
val userInfo: String
)
Comment on lines +10 to +12
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

Url 클래슀λ₯Ό GoogleOauthProperties λ‚΄λΆ€λ‘œ μ€‘μ²©μ‹œν‚€μ„Έμš”.

ν˜„μž¬ Url ν΄λž˜μŠ€κ°€ νŒ¨ν‚€μ§€ λ ˆλ²¨μ— μ •μ˜λ˜μ–΄ μžˆμ–΄ λ‹€μŒ λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€:

  • λ²”μš©μ μΈ μ΄λ¦„μœΌλ‘œ μΈν•œ λ„€μž„μŠ€νŽ˜μ΄μŠ€ 좩돌 μœ„ν—˜
  • GoogleOauthPropertiesμ™€μ˜ 관계가 λͺ…μ‹œμ μ΄μ§€ μ•ŠμŒ
  • μ½”λ“œ ꡬ쑰의 가독성 μ €ν•˜

Url 클래슀λ₯Ό GoogleOauthProperties λ‚΄λΆ€ 클래슀둜 μ€‘μ²©ν•˜μ—¬ 관계λ₯Ό λͺ…ν™•νžˆ ν•˜κ³  λ„€μž„μŠ€νŽ˜μ΄μŠ€λ₯Ό μ •λ¦¬ν•˜μ„Έμš”.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

 @ConfigurationProperties(prefix = "oauth.google")
 data class GoogleOauthProperties(
     val url: Url
-)
+) {
+    data class Url(
+        val userInfo: String
+    )
+}
-
-data class Url(
-    val userInfo: String
-)
πŸ€– Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/config/GoogleOauthProperties.kt around
lines 10 to 12, the top-level data class Url should be moved inside the
GoogleOauthProperties class to avoid namespace collisions and clarify the
relationship; edit GoogleOauthProperties to declare Url as a nested (or inner if
it needs access to outer instance) data class (e.g., data class Url(val
userInfo: String)) placed within the GoogleOauthProperties body, remove the
top-level Url declaration, and update any references/imports elsewhere to use
GoogleOauthProperties.Url (or GoogleOauthProperties().Url if made inner)
accordingly.

8 changes: 8 additions & 0 deletions apis/src/main/kotlin/org/yapp/apis/config/PropertiesConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.yapp.apis.config

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(GoogleOauthProperties::class)
class PropertiesConfig
10 changes: 10 additions & 0 deletions apis/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ swagger:
description: YAPP API Documentation for Development
version: v1.0.0-dev

oauth:
google:
url:
user-info: https://www.googleapis.com/oauth2/v2/userinfo

---
spring:
config:
Expand All @@ -85,3 +90,8 @@ springdoc:
enabled: false
api-docs:
enabled: false

oauth:
google:
url:
user-info: https://www.googleapis.com/oauth2/v2/userinfo
115 changes: 85 additions & 30 deletions apis/src/main/resources/static/kakao-login.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,38 +53,50 @@
color: #000;
}

.apple-btn {
background-color: #000;
color: #fff;
}
</style>
</head>
<body>

<h1 style="text-align:center">μ†Œμ…œ 둜그인 ν…ŒμŠ€νŠΈ</h1>

<div class="tabs">
<div class="tab active" data-tab="kakao">카카였 둜그인</div>
<div class="tab" data-tab="apple">μ• ν”Œ 둜그인</div>
</div>

<div class="tab-content active" id="kakao">
<p style="text-align:center;">카카였 κ³„μ •μœΌλ‘œ λ‘œκ·ΈμΈν•˜λ €λ©΄ μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.</p>
<div style="text-align:center;">
<button id="kakao-login-btn" class="btn kakao-btn">카카였 둜그인</button>
</div>
</div>

<div class="tab-content" id="apple">
<p style="text-align:center;">μ• ν”Œ κ³„μ •μœΌλ‘œ λ‘œκ·ΈμΈν•˜λ €λ©΄ μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.</p>
<div style="text-align:center;">
<button id="apple-login-btn" class="btn apple-btn">Apple 둜그인</button>
</div>
</div>

.apple-btn {
background-color: #000;
color: #fff;
}
.google-btn {
background-color: #4285F4;
color: #fff;
}
</style>
</head>
<body>

<h1 style="text-align:center">μ†Œμ…œ 둜그인 ν…ŒμŠ€νŠΈ</h1>

<div class="tabs">
<div class="tab active" data-tab="kakao">카카였 둜그인</div>
<div class="tab" data-tab="apple">μ• ν”Œ 둜그인</div>
<div class="tab" data-tab="google">ꡬ글 둜그인</div>
</div>

<div class="tab-content active" id="kakao">
<p style="text-align:center;">카카였 κ³„μ •μœΌλ‘œ λ‘œκ·ΈμΈν•˜λ €λ©΄ μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.</p>
<div style="text-align:center;">
<button id="kakao-login-btn" class="btn kakao-btn">카카였 둜그인</button>
</div>
</div>

<div class="tab-content" id="apple">
<p style="text-align:center;">μ• ν”Œ κ³„μ •μœΌλ‘œ λ‘œκ·ΈμΈν•˜λ €λ©΄ μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.</p>
<div style="text-align:center;">
<button id="apple-login-btn" class="btn apple-btn">Apple 둜그인</button>
</div>
</div>

<div class="tab-content" id="google">
<p style="text-align:center;">ꡬ글 κ³„μ •μœΌλ‘œ λ‘œκ·ΈμΈν•˜λ €λ©΄ μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.</p>
<div style="text-align:center;">
<button id="google-login-btn" class="btn google-btn">Google 둜그인</button>
</div>
</div>
<!-- κ²°κ³Ό 좜λ ₯ -->
<pre id="result" style="margin-top: 30px; white-space: pre-wrap;"></pre>

<meta name="google-signin-client_id" content="">
<!-- Kakao SDK -->
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>

Expand All @@ -93,6 +105,26 @@ <h1 style="text-align:center">μ†Œμ…œ 둜그인 ν…ŒμŠ€νŠΈ</h1>
src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>

<script>
// Google Sign-In Callback (Global function)
function onSignIn(googleUser) {
var id_token = googleUser.getAuthResponse().id_token;
fetch(`${API_SERVER}/api/v1/auth/signin`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
providerType: "GOOGLE",
oauthToken: id_token
})
})
.then(res => res.json())
.then(data => {
document.getElementById('result').textContent = 'Google 둜그인 성곡\n\nJWT: ' + data.accessToken;
})
.catch(err => {
document.getElementById('result').textContent = 'Google 둜그인 μ‹€νŒ¨: ' + err.message;
});
}

// νƒ­ μ „ν™˜
const tabs = document.querySelectorAll('.tab');
const contents = document.querySelectorAll('.tab-content');
Expand Down Expand Up @@ -238,7 +270,30 @@ <h1 style="text-align:center">μ†Œμ…œ 둜그인 ν…ŒμŠ€νŠΈ</h1>
});
}
});
</script>

// Google Sign-In Initialization
var GoogleAuth; // GoogleAuth object

function initGoogleAuth() {
gapi.client.init({
clientId: document.querySelector('meta[name="google-signin-client_id"]').content,
scope: 'profile email'
}).then(function () {
GoogleAuth = gapi.auth2.getAuthInstance();
// Attach the click listener to the Google login button
document.getElementById('google-login-btn').addEventListener('click', () => {
GoogleAuth.signIn().then(onSignIn, (error) => {
console.error('Google Sign-In failed:', error);
document.getElementById('result').textContent = 'Google 둜그인 μ‹€νŒ¨: ' + JSON.stringify(error);
});
});
});
}

function handleGoogleClientLoad() {
gapi.load('client:auth2', initGoogleAuth);
}
</script>
<script async defer src="https://apis.google.com/js/api.js" onload="this.onload=function(){};handleGoogleClientLoad();"></script>
</body>
</html>
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ sonar {
// SonarQube νƒœμŠ€ν¬κ°€ 톡합 JaCoCo λ¦¬ν¬νŠΈμ— μ˜μ‘΄ν•˜λ„λ‘ μ„€μ •
tasks.named("sonar") {
dependsOn("jacocoRootReport")
onlyIf { System.getenv("SONAR_TOKEN") != null }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ package org.yapp.domain.user
* Enum representing different authentication providers.
*/
enum class ProviderType {
KAKAO, APPLE
KAKAO, APPLE, GOOGLE
}
3 changes: 3 additions & 0 deletions infra/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dependencies {
implementation(Dependencies.Spring.BOOT_STARTER_DATA_REDIS)
implementation(Dependencies.Spring.KOTLIN_REFLECT)


implementation(Dependencies.Spring.BOOT_STARTER_OAUTH2_CLIENT)

Comment on lines +11 to +13
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

Spring OAuth2 Client μ˜μ‘΄μ„± μ‚¬μš© μ—¬λΆ€ 확인 μ œμ•ˆ

ν˜„μž¬ PR μ—μ„œ λ³Έ μ½”λ“œλ§Œ 보면 Google 연동은 RestClient 둜 직접 UserInfo μ—”λ“œν¬μΈνŠΈλ₯Ό ν˜ΈμΆœν•˜κ³  μžˆμ–΄, infra λͺ¨λ“ˆμ΄ BOOT_STARTER_OAUTH2_CLIENT 의 κΈ°λŠ₯을 μ‹€μ œλ‘œ μ“°μ§€ μ•ŠλŠ” κ²ƒμ²˜λŸΌ λ³΄μž…λ‹ˆλ‹€.
ν–₯ν›„ Security OAuth2 Client λ₯Ό μ‚¬μš©ν•  κ³„νšμ΄ μ—†λ‹€λ©΄, 이 μ˜μ‘΄μ„±μ€ μ œκ±°ν•΄μ„œ ν΄λž˜μŠ€νŒ¨μŠ€μ™€ λΉŒλ“œ μ‹œκ°„μ„ μ€„μ΄λŠ” 것도 κ³ λ €ν•΄ μ£Όμ„Έμš”.

implementation(Dependencies.RestClient.HTTP_CLIENT5)
implementation(Dependencies.RestClient.HTTP_CORE5)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.yapp.infra.external.oauth.google

import org.springframework.stereotype.Component
import org.yapp.infra.external.oauth.google.response.GoogleUserInfo

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

fun fetchUserInfo(
accessToken: String,
userInfoUrl: String,
): Result<GoogleUserInfo> {
return runCatching {
googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl)
}
}
Comment on lines +14 to +21
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

μ•‘μ„ΈμŠ€ 토큰 검증을 μΆ”κ°€ν•˜μ„Έμš”.

accessToken을 BEARER_PREFIX와 직접 μ—°κ²°ν•˜κΈ° 전에 λ‹€μŒμ„ 확인해야 ν•©λ‹ˆλ‹€:

  • 토큰이 이미 "Bearer " ν”„λ¦¬ν”½μŠ€λ₯Ό ν¬ν•¨ν•˜κ³  μžˆμ§€ μ•Šμ€μ§€
  • 토큰이 λΉ„μ–΄μžˆμ§€ μ•Šμ€μ§€

잘λͺ»λœ Authorization ν—€λ”λ‘œ μΈν•œ API 호좜 μ‹€νŒ¨λ₯Ό λ°©μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 검증 λ‘œμ§μ„ μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

 fun fetchUserInfo(
     accessToken: String,
     userInfoUrl: String,
 ): Result<GoogleUserInfo> {
+    require(accessToken.isNotBlank()) { "Access token must not be blank" }
+    val token = if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
+        accessToken
+    } else {
+        BEARER_PREFIX + accessToken
+    }
     return runCatching {
-        googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl)
+        googleRestClient.getUserInfo(token, userInfoUrl)
     }
 }
πŸ“ 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 fetchUserInfo(
accessToken: String,
userInfoUrl: String,
): Result<GoogleUserInfo> {
return runCatching {
googleRestClient.getUserInfo(BEARER_PREFIX + accessToken, userInfoUrl)
}
}
fun fetchUserInfo(
accessToken: String,
userInfoUrl: String,
): Result<GoogleUserInfo> {
require(accessToken.isNotBlank()) { "Access token must not be blank" }
val token = if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
accessToken
} else {
BEARER_PREFIX + accessToken
}
return runCatching {
googleRestClient.getUserInfo(token, userInfoUrl)
}
}
πŸ€– Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleApi.kt
around lines 14 to 21, add validation and normalization for accessToken before
concatenating with BEARER_PREFIX: first trim and ensure it's not blank (return a
failed Result or throw an IllegalArgumentException inside runCatching if blank),
then if it already starts with "Bearer " (case-insensitive) use it as-is,
otherwise prepend BEARER_PREFIX; finally pass the normalized authorization
header into googleRestClient.getUserInfo so we avoid empty or double-prefixed
Authorization headers.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.yapp.infra.external.oauth.google

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

@Component
class GoogleRestClient(
builder: RestClient.Builder
) {
private val client = builder.build()
Comment on lines +7 to +11
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

νƒ€μž„μ•„μ›ƒ 섀정을 μΆ”κ°€ν•˜μ„Έμš”.

μ™ΈλΆ€ API 호좜 μ‹œ νƒ€μž„μ•„μ›ƒμ΄ μ„€μ •λ˜μ–΄ μžˆμ§€ μ•ŠμœΌλ©΄ 응닡이 μ—†λŠ” 경우 λ¬΄ν•œμ • λŒ€κΈ°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ—°κ²° νƒ€μž„μ•„μ›ƒκ³Ό 읽기 νƒ€μž„μ•„μ›ƒμ„ μ„€μ •ν•˜μ—¬ μ‹œμŠ€ν…œ μ•ˆμ •μ„±μ„ ν–₯μƒμ‹œν‚€μ„Έμš”.

λ‹€μŒκ³Ό 같이 νƒ€μž„μ•„μ›ƒμ„ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

+import java.time.Duration
+
 @Component
 class GoogleRestClient(
     builder: RestClient.Builder
 ) {
-    private val client = builder.build()
+    private val client = builder
+        .requestFactory(
+            org.springframework.http.client.SimpleClientHttpRequestFactory().apply {
+                setConnectTimeout(Duration.ofSeconds(5))
+                setReadTimeout(Duration.ofSeconds(10))
+            }
+        )
+        .build()

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt
around lines 7 to 11, the RestClient is built without any timeout configuration;
update the builder to set sensible connection and read (socket) timeouts before
calling build (for example short connect timeout and a longer read timeout),
using the RestClient.Builder timeout methods available in your HTTP client
(e.g., setConnectTimeout / setReadTimeout or the Duration-based equivalents), so
the client fails fast on network issues and avoids indefinite waits.


fun getUserInfo(
bearerToken: String,
url: String,
): GoogleUserInfo {
return client.get()
.uri(url)
.header("Authorization", bearerToken)
.retrieve()
.body(GoogleUserInfo::class.java)
?: throw IllegalStateException("Google API 응닡이 null μž…λ‹ˆλ‹€.")
}
Comment on lines +13 to +23
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | πŸ”΅ Trivial

μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό 영문으둜 μž‘μ„±ν•˜κ±°λ‚˜ μ—λŸ¬ μ½”λ“œλ₯Ό μ‚¬μš©ν•˜μ„Έμš”.

Line 22의 IllegalStateException λ©”μ‹œμ§€κ°€ ν•œκΈ€λ‘œ μž‘μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. 인프라 λ ˆμ΄μ–΄μ˜ μ˜ˆμ™Έ λ©”μ‹œμ§€λŠ” 영문으둜 μž‘μ„±ν•˜κ±°λ‚˜, μƒμœ„ λ ˆμ΄μ–΄μ—μ„œ μ²˜λ¦¬ν•  수 μžˆλ„λ‘ 도메인별 μ˜ˆμ™Έλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

     return client.get()
         .uri(url)
         .header("Authorization", bearerToken)
         .retrieve()
         .body(GoogleUserInfo::class.java)
-        ?: throw IllegalStateException("Google API 응닡이 null μž…λ‹ˆλ‹€.")
+        ?: throw IllegalStateException("Google API response body is null")
 }
πŸ“ 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 getUserInfo(
bearerToken: String,
url: String,
): GoogleUserInfo {
return client.get()
.uri(url)
.header("Authorization", bearerToken)
.retrieve()
.body(GoogleUserInfo::class.java)
?: throw IllegalStateException("Google API 응닡이 null μž…λ‹ˆλ‹€.")
}
fun getUserInfo(
bearerToken: String,
url: String,
): GoogleUserInfo {
return client.get()
.uri(url)
.header("Authorization", bearerToken)
.retrieve()
.body(GoogleUserInfo::class.java)
?: throw IllegalStateException("Google API response body is null")
}
πŸ€– Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/external/oauth/google/GoogleRestClient.kt
around lines 13 to 23, replace the Korean Literal in the thrown
IllegalStateException with an English message or throw a domain-specific
exception so infra-layer errors are consistent and can be handled by upper
layers; update the throw to something like IllegalStateException("Google API
response is null.") or define and throw a specific exception (e.g.,
GoogleApiException("response is null")) and ensure callers can handle the new
exception type.

}
Loading