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 @@ -50,15 +50,15 @@ class ApiModule {
ktorfit.createRefreshService()

@Single
fun authService(ktorfit: Ktorfit): AuthService =
fun authService(@AuthKtorfit ktorfit: Ktorfit): AuthService =
ktorfit.createAuthService()

@Single
fun eldersInfoService(ktorfit: Ktorfit): EldersInfoService =
ktorfit.createEldersInfoService()

@Single
fun memberRegisterService(ktorfit: Ktorfit): MemberRegisterService =
fun memberRegisterService(@AuthKtorfit ktorfit: Ktorfit): MemberRegisterService =
ktorfit.createMemberRegisterService()

@Single
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,28 @@ class NetworkModule {

refreshTokens {
refreshMutex.withLock {
// 3. 현재 저장된 토큰들을 가져옴
val accessToken = dataStoreRepository.getAccessToken()
val refreshToken = dataStoreRepository.getRefreshToken()
if (refreshToken.isNullOrBlank()) {
dataStoreRepository.saveRefreshToken("")
return@withLock null
}

require(!refreshToken.isNullOrEmpty())
val refreshResponse = refreshService.refreshToken(refreshToken)
val refreshResponse = runCatching {
refreshService.refreshToken(refreshToken)
}.getOrElse {
dataStoreRepository.saveRefreshToken("")
return@withLock null
Comment on lines +83 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

refresh 실패 시 토큰을 부분적으로만 지우면 상태가 꼬입니다.

여기서는 빈 refresh token, 예외, 실패 응답을 전부 saveRefreshToken("")로 처리하고 있어서 access token은 그대로 남습니다. 그러면 토큰 저장소가 반쯤만 비워진 상태가 되고, 일시적인 네트워크/서버 장애까지 세션 만료로 오인하게 돼요. 재발급이 확실히 불가능한 경우에만 clearTokens()로 둘을 함께 정리하고, 그 외 실패는 저장값을 유지한 채 null만 반환하는 쪽이 더 안전합니다.

Also applies to: 104-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/konkuk/medicarecall/data/di/NetworkModule.kt`
around lines 83 - 92, The current withLock block clears only the refresh token
(dataStoreRepository.saveRefreshToken("")) on empty/exception/failure cases
which leaves the access token behind and causes inconsistent state; update the
logic in the withLock that calls refreshService.refreshToken(refreshToken) so
that: 1) if refreshToken.isNullOrBlank() then call clearTokens() to remove both
tokens and return null; 2) when runCatching { refreshService.refreshToken(...) }
fails or the refresh response indicates unrecoverable error, call clearTokens()
and return null; 3) for transient failures (e.g., network exception or non-fatal
error) avoid mutating stored tokens — do not call saveRefreshToken("") — and
simply return null so the existing tokens remain; apply the same adjustment to
the analogous block referenced at lines 104-107. Ensure you use
dataStoreRepository.clearTokens(), dataStoreRepository.saveRefreshToken(...),
refreshService.refreshToken(...), and the enclosing withLock to locate and
modify the code.

}

if (refreshResponse.isSuccessful && refreshResponse.body() != null) {
// 토큰 갱신 성공 시, 새로운 토큰들을 DataStore에 저장
val newTokens = refreshResponse.body()!!
dataStoreRepository.saveAccessToken(newTokens.accessToken)
dataStoreRepository.saveRefreshToken(newTokens.refreshToken)

val newAccessToken = dataStoreRepository.getAccessToken()
val newRefreshToken = dataStoreRepository.getRefreshToken()

BearerTokens(newAccessToken ?: "", newRefreshToken)
BearerTokens(
accessToken = newTokens.accessToken,
refreshToken = newTokens.refreshToken,
)
} else {
// 토큰 갱신 실패 시, 저장된 토큰 삭제 후 null 반환
dataStoreRepository.saveRefreshToken("")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.konkuk.medicarecall.data.api.auth.AuthService
import com.konkuk.medicarecall.data.api.member.SettingService
import com.konkuk.medicarecall.data.mapper.UserMapper
import com.konkuk.medicarecall.data.repository.DataStoreRepository
import com.konkuk.medicarecall.data.repository.ElderIdRepository
import com.konkuk.medicarecall.data.repository.UserRepository
import com.konkuk.medicarecall.data.util.handleNullableResponse
import com.konkuk.medicarecall.data.util.handleResponse
Expand All @@ -15,6 +16,7 @@ class UserRepositoryImpl(
private val settingService: SettingService,
private val authService: AuthService,
private val tokenStore: DataStoreRepository,
private val elderIdRepository: ElderIdRepository,
) : UserRepository {
override suspend fun getMyInfo() = runCatching {
val responseDto = settingService.getMyInfo().handleResponse()
Expand All @@ -27,10 +29,18 @@ class UserRepositoryImpl(
UserMapper.toDomain(responseDto)
}

override suspend fun logout(): Result<Unit> = runCatching {
val refresh = tokenStore.getRefreshToken() ?: error("Refresh token is null")
authService.logout("Bearer $refresh").handleNullableResponse()
// 성공/실패와 무관하게 로컬 토큰 제거(보안/UX 측면에서 권장)
override suspend fun logout(): Result<Unit> {
runCatching {
val refresh = tokenStore.getRefreshToken()
if (!refresh.isNullOrBlank()) {
authService.logout("Bearer $refresh").handleNullableResponse()
}
}

tokenStore.clearTokens()
elderIdRepository.clearElderIds()

return Result.success(Unit)
Comment on lines +33 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로컬 cleanup도 Result 계약 안으로 넣어 주세요.

지금은 서버 로그아웃만 runCatching 안에 있고 clearTokens() / clearElderIds()는 밖에 있어서, 저장소 I/O가 실패하면 logout()Result.failure가 아니라 예외를 그대로 던집니다. 게다가 첫 cleanup이 실패하면 다음 cleanup도 건너뛰게 돼요. 로컬 정리까지 개별적으로 시도한 뒤 최종 Result를 만드는 편이 더 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/konkuk/medicarecall/data/repositoryimpl/UserRepositoryImpl.kt`
around lines 33 - 44, logout() currently wraps only the network call in
runCatching so tokenStore.clearTokens() and elderIdRepository.clearElderIds()
can throw and escape the Result contract; wrap the local cleanup inside the same
exception-safe flow (or perform each cleanup in its own runCatching and
aggregate errors) so that failures from tokenStore.clearTokens() or
elderIdRepository.clearElderIds() are captured and logout() returns a
corresponding Result.failure, while still attempting both cleanups (i.e., call
authService.logout(...).handleNullableResponse() and then run
tokenStore.clearTokens() and elderIdRepository.clearElderIds() under runCatching
blocks and convert any thrown exceptions into a single Result).

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.konkuk.medicarecall.domain.usecase

import com.konkuk.medicarecall.data.repository.ElderIdRepository
import com.konkuk.medicarecall.data.repository.EldersInfoRepository
import com.konkuk.medicarecall.data.repository.DataStoreRepository
import com.konkuk.medicarecall.ui.model.NavigationDestination
import org.koin.core.annotation.Factory
import com.konkuk.medicarecall.data.exception.HttpException
Expand All @@ -10,13 +11,21 @@ import com.konkuk.medicarecall.data.exception.HttpException
class CheckLoginStatusUseCase(
private val eldersInfoRepository: EldersInfoRepository,
private val elderIdRepository: ElderIdRepository,
private val dataStoreRepository: DataStoreRepository,
) {
/**
* 앱의 초기 상태를 확인하여 다음에 이동할 화면을 결정합니다.
* 이 과정에서 어떤 종류의 에러가 발생하더라도 로그인 화면으로 안내합니다.
*/
suspend operator fun invoke(): NavigationDestination {
return runCatching {
val accessToken = dataStoreRepository.getAccessToken()
val refreshToken = dataStoreRepository.getRefreshToken()

if (accessToken.isNullOrBlank() || refreshToken.isNullOrBlank()) {
return@runCatching NavigationDestination.GoToLogin
}

// 1. 어르신 정보 확인
val elders = eldersInfoRepository.getElders().getOrThrow()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,8 @@ class MainNavigator(

fun navigateToLoginAfterLogout() {
navController.navigate(Route.LoginStart) {
popUpTo(MainTabRoute.Home) { inclusive = true }
popUpTo(0) { inclusive = true }
launchSingleTop = true
restoreState = true
}
}

Expand Down
Loading