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