diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..0c4235cf --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..44ca2d9b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 00000000..4604c446 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,252 @@ + + + + + + \ No newline at end of file diff --git a/core/model/src/main/java/com/luckyoct/core/model/request/TokenReissueRequest.kt b/core/model/src/main/java/com/luckyoct/core/model/request/TokenReissueRequest.kt new file mode 100644 index 00000000..290f89e1 --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/core/model/request/TokenReissueRequest.kt @@ -0,0 +1,8 @@ +package com.luckyoct.core.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class TokenReissueRequest( + val refreshToken: String +) diff --git a/core/model/src/main/java/com/luckyoct/core/model/response/TokenReissue.kt b/core/model/src/main/java/com/luckyoct/core/model/response/TokenReissue.kt new file mode 100644 index 00000000..d400926c --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/core/model/response/TokenReissue.kt @@ -0,0 +1,9 @@ +package com.luckyoct.core.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class TokenReissue( + val accessToken: String, + val refreshToken: String +) diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt index 7afd645d..baa1df7d 100644 --- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt @@ -1,20 +1,26 @@ package com.goalpanzi.mission_mate.core.network import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource +import com.goalpanzi.mission_mate.core.network.service.TokenService +import com.luckyoct.core.model.request.TokenReissueRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.Interceptor +import okhttp3.Request import okhttp3.Response +import java.net.HttpURLConnection import javax.inject.Inject import javax.inject.Singleton @Singleton class TokenInterceptor @Inject constructor( - private val authDataSource: AuthDataSource + private val authDataSource: AuthDataSource, + private val tokenService: TokenService ) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { val newRequest = chain.request().newBuilder().apply { runBlocking { @@ -26,13 +32,35 @@ class TokenInterceptor @Inject constructor( } val response = chain.proceed(newRequest.build()) - if (response.code == 200) { - val newAccessToken: String = response.header("Authorization", null) ?: return response - CoroutineScope(Dispatchers.IO).launch { - val existedAccessToken = authDataSource.getAccessToken().first() - if (existedAccessToken != newAccessToken) { - authDataSource.setAccessToken(newAccessToken) + + when (response.code) { + HttpURLConnection.HTTP_OK -> { + val newAccessToken: String = response.header("Authorization", null) ?: return response + CoroutineScope(Dispatchers.IO).launch { + val existedAccessToken = authDataSource.getAccessToken().first() + if (existedAccessToken != newAccessToken) { + authDataSource.setAccessToken(newAccessToken) + } + } + } + HttpURLConnection.HTTP_UNAUTHORIZED -> { + val retryRequest = chain.request().newBuilder().apply { + runBlocking { + authDataSource.getRefreshToken().first()?.let { + val newToken = tokenService.requestTokenReissue(TokenReissueRequest(it)) + if (newToken.isSuccessful) { + newToken.body()?.let { token -> + addHeader("Authorization", "Bearer ${token.accessToken}") + CoroutineScope(Dispatchers.IO).launch { + authDataSource.setAccessToken(token.accessToken) + authDataSource.setRefreshToken(token.refreshToken) + } + } + } + } + } } + return chain.proceed(retryRequest.build()) } } return response diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt index ab49da5c..433fcaca 100644 --- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt @@ -1,13 +1,24 @@ package com.goalpanzi.mission_mate.core.network.di +import android.util.Log +import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource +import com.goalpanzi.mission_mate.core.network.BuildConfig import com.goalpanzi.mission_mate.core.network.service.LoginService import com.goalpanzi.mission_mate.core.network.service.MissionService import com.goalpanzi.mission_mate.core.network.service.OnboardingService import com.goalpanzi.mission_mate.core.network.service.ProfileService +import com.goalpanzi.mission_mate.core.network.service.TokenService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter import retrofit2.Retrofit import javax.inject.Singleton @@ -38,4 +49,35 @@ object ServiceModule { fun provideMissionService(retrofit: Retrofit): MissionService { return retrofit.create(MissionService::class.java) } + + @Provides + @Singleton + fun provideTokenService( + httpLoggingInterceptor: HttpLoggingInterceptor, + converterFactory: Converter.Factory, + authDataSource: AuthDataSource + ): TokenService { + val tokenReissueInterceptor = Interceptor { chain -> + val newRequest = chain.request().newBuilder().apply { + runBlocking { + val token = authDataSource.getAccessToken().first() + token?.let { + addHeader("Authorization", "Bearer $it") + } + } + } + chain.proceed(newRequest.build()) + } + val retrofit = Retrofit.Builder() + .client( + OkHttpClient.Builder() + .addInterceptor(tokenReissueInterceptor) + .addInterceptor(httpLoggingInterceptor) + .build() + ) + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(converterFactory) + .build() + return retrofit.create(TokenService::class.java) + } } \ No newline at end of file diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt new file mode 100644 index 00000000..011b38c6 --- /dev/null +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt @@ -0,0 +1,14 @@ +package com.goalpanzi.mission_mate.core.network.service + +import com.luckyoct.core.model.request.TokenReissueRequest +import com.luckyoct.core.model.response.TokenReissue +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface TokenService { + @POST("/api/auth/token:reissue") + suspend fun requestTokenReissue( + @Body request: TokenReissueRequest + ): Response +} \ No newline at end of file