diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/AuthenticationApi.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/AuthenticationApi.kt index 436b6eb9..9bfa8be2 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/AuthenticationApi.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/AuthenticationApi.kt @@ -1,18 +1,22 @@ package ac.dnd.bookkeeping.android.data.remote.network.api +import ac.dnd.bookkeeping.android.data.remote.network.di.AuthHttpClient import ac.dnd.bookkeeping.android.data.remote.network.di.NoAuthHttpClient import ac.dnd.bookkeeping.android.data.remote.network.environment.BaseUrlProvider import ac.dnd.bookkeeping.android.data.remote.network.environment.ErrorMessageMapper -import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.GetAccessTokenReq import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.GetAccessTokenRes +import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.LoginReq +import ac.dnd.bookkeeping.android.data.remote.network.model.authentication.LoginRes import ac.dnd.bookkeeping.android.data.remote.network.util.convert import io.ktor.client.HttpClient +import io.ktor.client.request.header import io.ktor.client.request.post import io.ktor.client.request.setBody import javax.inject.Inject class AuthenticationApi @Inject constructor( - @NoAuthHttpClient private val client: HttpClient, + @NoAuthHttpClient private val noAuthClient: HttpClient, + @AuthHttpClient private val client: HttpClient, private val baseUrlProvider: BaseUrlProvider, private val errorMessageMapper: ErrorMessageMapper ) { @@ -22,12 +26,34 @@ class AuthenticationApi @Inject constructor( suspend fun getAccessToken( refreshToken: String ): Result { - return client.post("$baseUrl/member/v1/refresh") { + return noAuthClient.post("$baseUrl/api/v1/token/reissue") { + header("Token-Refresh", refreshToken) + }.convert(errorMessageMapper::map) + } + + suspend fun login( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + return noAuthClient.post("$baseUrl/api/v1/auth/login") { setBody( - GetAccessTokenReq( - refreshToken = refreshToken + LoginReq( + socialId = socialId, + email = email, + profileImageUrl = profileImageUrl ) ) }.convert(errorMessageMapper::map) } + + suspend fun logout(): Result { + return client.post("$baseUrl/api/v1/auth/logout") + .convert(errorMessageMapper::map) + } + + suspend fun withdraw(): Result { + return client.post("$baseUrl/api/v1/members/me") + .convert(errorMessageMapper::map) + } } diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/di/KtorModule.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/di/KtorModule.kt index 114f5b49..81492599 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/di/KtorModule.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/di/KtorModule.kt @@ -1,8 +1,8 @@ package ac.dnd.bookkeeping.android.data.remote.network.di +import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository import android.content.Context import android.content.pm.ApplicationInfo -import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -63,7 +63,8 @@ internal object KtorModule { @DebugInterceptor debugInterceptor: Optional, authenticationRepository: AuthenticationRepository ): HttpClient { - val isDebug: Boolean = (0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) + val isDebug: Boolean = + (0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) return HttpClient(OkHttp) { // default validation to throw exceptions for non-2xx responses @@ -89,7 +90,10 @@ internal object KtorModule { return@loadTokens null } - BearerTokens(accessToken, refreshToken) + BearerTokens( + accessToken = accessToken, + refreshToken = refreshToken + ) } refreshTokens { @@ -98,12 +102,13 @@ internal object KtorModule { return@refreshTokens null } - authenticationRepository.getAccessToken( + authenticationRepository.refreshToken( refreshToken - ).getOrNull()?.let { accessToken -> - authenticationRepository.accessToken = accessToken - - BearerTokens(accessToken, refreshToken) + ).getOrNull()?.let { token -> + BearerTokens( + accessToken = token.accessToken, + refreshToken = token.refreshToken + ) } } } diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenRes.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenRes.kt index 2aaa0cf1..7054bae0 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenRes.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenRes.kt @@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable @Serializable data class GetAccessTokenRes( - @SerialName("access_token") - val accessToken: String + @SerialName("accessToken") + val accessToken: String, + @SerialName("refreshToken") + val refreshToken: String ) diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginReq.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginReq.kt new file mode 100644 index 00000000..27317936 --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginReq.kt @@ -0,0 +1,14 @@ +package ac.dnd.bookkeeping.android.data.remote.network.model.authentication + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LoginReq( + @SerialName("socialId") + val socialId: String, + @SerialName("email") + val email: String, + @SerialName("profileImageUrl") + val profileImageUrl: String +) diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenReq.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginRes.kt similarity index 55% rename from data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenReq.kt rename to data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginRes.kt index 7d528508..35338d21 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/GetAccessTokenReq.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/authentication/LoginRes.kt @@ -4,7 +4,11 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class GetAccessTokenReq( - @SerialName("refresh_token") +data class LoginRes( + @SerialName("isNew") + val isNew: Boolean, + @SerialName("accessToken") + val accessToken: String, + @SerialName("refreshToken") val refreshToken: String ) diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/error/ErrorRes.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/error/ErrorRes.kt index 179b35b5..05ce3a54 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/error/ErrorRes.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/error/ErrorRes.kt @@ -5,8 +5,8 @@ import kotlinx.serialization.Serializable @Serializable data class ErrorRes( - @SerialName("id") - val id: String, + @SerialName("code") + val code: String, @SerialName("message") val message: String ) diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/util/NetworkUtil.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/util/NetworkUtil.kt index ebd5b81f..17eea07e 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/util/NetworkUtil.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/util/NetworkUtil.kt @@ -56,7 +56,7 @@ suspend inline fun HttpResponse.toThrowable( } return@runCatching this.body()?.let { errorRes -> - BadRequestServerException(errorRes.id, errorMessageMapper(errorRes.id)) + BadRequestServerException(errorRes.code, errorMessageMapper(errorRes.code)) } ?: InvalidStandardResponseException("Response Empty Body") }.getOrElse { exception -> exception diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/MockAuthenticationRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/MockAuthenticationRepository.kt index 180eb1f0..e57feb1d 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/MockAuthenticationRepository.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/MockAuthenticationRepository.kt @@ -1,6 +1,8 @@ package ac.dnd.bookkeeping.android.data.repository.authentication import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager +import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken +import ac.dnd.bookkeeping.android.domain.model.authentication.Login import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository import kotlinx.coroutines.delay import javax.inject.Inject @@ -17,11 +19,37 @@ class MockAuthenticationRepository @Inject constructor( set(value) = sharedPreferencesManager.setString(ACCESS_TOKEN, value) get() = sharedPreferencesManager.getString(ACCESS_TOKEN, "") - override suspend fun getAccessToken( + override suspend fun refreshToken( refreshToken: String - ): Result { + ): Result { randomShortDelay() - return Result.success("refresh_token") + return Result.success( + JwtToken( + accessToken = "mock_access_token", + refreshToken = "mock_refresh_token" + ) + ) + } + + override suspend fun login( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + randomShortDelay() + return Result.success( + Login(isNew = true) + ) + } + + override suspend fun logout(): Result { + randomShortDelay() + return Result.success(Unit) + } + + override suspend fun withdraw(): Result { + randomLongDelay() + return Result.success(Unit) } private suspend fun randomShortDelay() { diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/RealAuthenticationRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/RealAuthenticationRepository.kt index b90dec37..a9ae1969 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/RealAuthenticationRepository.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/authentication/RealAuthenticationRepository.kt @@ -2,6 +2,8 @@ package ac.dnd.bookkeeping.android.data.repository.authentication import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager import ac.dnd.bookkeeping.android.data.remote.network.api.AuthenticationApi +import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken +import ac.dnd.bookkeeping.android.domain.model.authentication.Login import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository import javax.inject.Inject @@ -13,20 +15,60 @@ class RealAuthenticationRepository @Inject constructor( override var refreshToken: String set(value) = sharedPreferencesManager.setString(REFRESH_TOKEN, value) get() = sharedPreferencesManager.getString(REFRESH_TOKEN, "") + override var accessToken: String set(value) = sharedPreferencesManager.setString(ACCESS_TOKEN, value) get() = sharedPreferencesManager.getString(ACCESS_TOKEN, "") - override suspend fun getAccessToken( + override suspend fun refreshToken( refreshToken: String - ): Result { + ): Result { return authenticationApi.getAccessToken( refreshToken = refreshToken - ).map { - it.accessToken + ).onSuccess { token -> + this.refreshToken = token.refreshToken + this.accessToken = token.accessToken + }.map { token -> + JwtToken( + accessToken = token.accessToken, + refreshToken = token.refreshToken + ) + } + } + + override suspend fun login( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + return authenticationApi.login( + socialId = socialId, + email = email, + profileImageUrl = profileImageUrl + ).onSuccess { token -> + this.refreshToken = token.refreshToken + this.accessToken = token.accessToken + }.map { login -> + Login(isNew = login.isNew) } } + override suspend fun logout(): Result { + return authenticationApi.logout() + .onSuccess { + this.refreshToken = "" + this.accessToken = "" + } + } + + override suspend fun withdraw(): Result { + return authenticationApi.withdraw() + .onSuccess { + this.refreshToken = "" + this.accessToken = "" + } + } + companion object { private const val REFRESH_TOKEN = "refresh_token" private const val ACCESS_TOKEN = "access_token" diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/JwtToken.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/JwtToken.kt new file mode 100644 index 00000000..62a799a1 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/JwtToken.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.domain.model.authentication + +data class JwtToken( + val accessToken: String, + val refreshToken: String +) diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/Login.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/Login.kt new file mode 100644 index 00000000..87678d36 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/authentication/Login.kt @@ -0,0 +1,5 @@ +package ac.dnd.bookkeeping.android.domain.model.authentication + +data class Login( + val isNew: Boolean +) diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/AuthenticationRepository.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/AuthenticationRepository.kt index 241c457d..b5918815 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/AuthenticationRepository.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/AuthenticationRepository.kt @@ -1,12 +1,25 @@ package ac.dnd.bookkeeping.android.domain.repository +import ac.dnd.bookkeeping.android.domain.model.authentication.JwtToken +import ac.dnd.bookkeeping.android.domain.model.authentication.Login + interface AuthenticationRepository { var refreshToken: String var accessToken: String - suspend fun getAccessToken( + suspend fun refreshToken( refreshToken: String - ): Result + ): Result + + suspend fun login( + socialId: String, + email: String, + profileImageUrl: String + ): Result + + suspend fun logout(): Result + + suspend fun withdraw(): Result } diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LoginUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LoginUseCase.kt new file mode 100644 index 00000000..cd23c9b0 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LoginUseCase.kt @@ -0,0 +1,21 @@ +package ac.dnd.bookkeeping.android.domain.usecase.authentication + +import ac.dnd.bookkeeping.android.domain.model.authentication.Login +import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository +import javax.inject.Inject + +class LoginUseCase @Inject constructor( + private val authenticationRepository: AuthenticationRepository +) { + suspend operator fun invoke( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + return authenticationRepository.login( + socialId = socialId, + email = email, + profileImageUrl = profileImageUrl + ) + } +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LogoutUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LogoutUseCase.kt new file mode 100644 index 00000000..5281b258 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/LogoutUseCase.kt @@ -0,0 +1,21 @@ +package ac.dnd.bookkeeping.android.domain.usecase.authentication + +import ac.dnd.bookkeeping.android.domain.model.authentication.Login +import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val authenticationRepository: AuthenticationRepository +) { + suspend operator fun invoke( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + return authenticationRepository.login( + socialId = socialId, + email = email, + profileImageUrl = profileImageUrl + ) + } +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/IsLoginSucceedUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/UpdateJwtTokenUseCase.kt similarity index 61% rename from domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/IsLoginSucceedUseCase.kt rename to domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/UpdateJwtTokenUseCase.kt index 0da15aef..2ef8a21f 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/IsLoginSucceedUseCase.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/UpdateJwtTokenUseCase.kt @@ -1,15 +1,14 @@ package ac.dnd.bookkeeping.android.domain.usecase.authentication import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository -import kotlinx.coroutines.delay import javax.inject.Inject -class IsLoginSucceedUseCase @Inject constructor( +class UpdateJwtTokenUseCase @Inject constructor( private val authenticationRepository: AuthenticationRepository ) { suspend operator fun invoke(): Result { - // TODO: Implement this - delay(1000L) - return Result.success(Unit) + return authenticationRepository.refreshToken( + refreshToken = authenticationRepository.refreshToken + ).map { } } } diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/WithdrawUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/WithdrawUseCase.kt new file mode 100644 index 00000000..65edb8f9 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/authentication/WithdrawUseCase.kt @@ -0,0 +1,21 @@ +package ac.dnd.bookkeeping.android.domain.usecase.authentication + +import ac.dnd.bookkeeping.android.domain.model.authentication.Login +import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository +import javax.inject.Inject + +class WithdrawUseCase @Inject constructor( + private val authenticationRepository: AuthenticationRepository +) { + suspend operator fun invoke( + socialId: String, + email: String, + profileImageUrl: String + ): Result { + return authenticationRepository.login( + socialId = socialId, + email = email, + profileImageUrl = profileImageUrl + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashScreen.kt index 0e75f1c3..b9e4f7b9 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashScreen.kt @@ -84,12 +84,12 @@ private fun Observer( navigateToHome() } - is SplashEvent.Login.Error -> { + is SplashEvent.Login.Failure -> { navigateToLogin() } - is SplashEvent.Login.Failure -> { - // TODO : Dialog Screen + is SplashEvent.Login.Error -> { + // TODO : Unknown Error (Client Error, Internal Server Error, ...) } } } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashViewModel.kt index 957a6cce..e6b05af4 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashViewModel.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/splash/SplashViewModel.kt @@ -1,7 +1,7 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.splash import ac.dnd.bookkeeping.android.domain.model.error.ServerException -import ac.dnd.bookkeeping.android.domain.usecase.authentication.IsLoginSucceedUseCase +import ac.dnd.bookkeeping.android.domain.usecase.authentication.UpdateJwtTokenUseCase import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow @@ -16,7 +16,7 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, - private val isLoginSucceedUseCase: IsLoginSucceedUseCase + private val updateJwtTokenUseCase: UpdateJwtTokenUseCase ) : BaseViewModel() { private val _state: MutableStateFlow = MutableStateFlow(SplashState.Init) @@ -31,12 +31,12 @@ class SplashViewModel @Inject constructor( init { launch { - login() + checkJwtToken() } } - private suspend fun login() { - isLoginSucceedUseCase().onSuccess { + private suspend fun checkJwtToken() { + updateJwtTokenUseCase().onSuccess { _event.emit(SplashEvent.Login.Success) }.onFailure { exception -> when (exception) {