diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/ServerApiClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/ServerApiClient.kt deleted file mode 100644 index b2593acb..00000000 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/ServerApiClient.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.stslex.core.network.api.server - -import Wizard.core.network.BuildConfig -import com.stslex.core.core.AppDispatcher -import com.stslex.core.network.api.base.NetworkClient -import com.stslex.core.network.api.base.NetworkClientBuilder.setupLogging -import com.stslex.core.network.api.base.NetworkClientBuilder.setupNegotiation -import com.stslex.core.network.utils.token.AuthController -import com.stslex.core.network.utils.token.toModel -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.HttpResponseValidator -import io.ktor.client.plugins.ResponseException -import io.ktor.client.plugins.auth.Auth -import io.ktor.client.plugins.auth.providers.BearerTokens -import io.ktor.client.plugins.auth.providers.bearer -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.request.HttpRequest -import io.ktor.client.request.bearerAuth -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.URLProtocol -import io.ktor.http.contentType -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -interface ServerApiClient : NetworkClient - -class ServerApiClientImpl( - private val tokenProvider: AuthController, - private val appDispatcher: AppDispatcher -) : ServerApiClient { - - private var refreshJob: Job? = null - - val client = HttpClient(CIO) { - setupNegotiation() - setupLogging() - expectSuccess = true - HttpResponseValidator { - handleResponseExceptionWithRequest(errorParser) - } - defaultRequest { - url( - scheme = URLProtocol.HTTP.name, - host = BuildConfig.SERVER_HOST, - port = BuildConfig.SERVER_PORT.toInt(), - path = BuildConfig.SERVER_API_VERSION, - block = { - contentType(ContentType.Application.Json) - } - ) - headers { - append(API_KEY_NAME, BuildConfig.SERVER_API_KEY) - } - } - } - - private val authClient: HttpClient - get() = client.config { - install(Auth) { - bearer { - loadTokens { - BearerTokens( - accessToken = tokenProvider.accessToken, - refreshToken = tokenProvider.refreshToken - ) - } - } - } - } - - override suspend fun request( - request: suspend HttpClient.() -> T - ): T = withContext(appDispatcher.io) { - try { - request(authClient) - } catch (error: ErrorRepeatEnd) { - request(authClient) - } - } - - private val errorParser: suspend (Throwable, HttpRequest) -> Unit - get() = { exception, _ -> - val clientException = exception as? ResponseException ?: throw exception - if (HttpStatusCode.Unauthorized.value == clientException.response.status.value) { - refreshToken() - } else { - throw clientException - } - } - - private suspend fun refreshToken() { - if (refreshJob?.isActive == true) return - refreshJob = coroutineScope { - launch { - val tokenResponse = client - .config { - HttpResponseValidator { - handleResponseExceptionWithRequest(refreshTokenValidator) - } - } - .get("passport/refresh") { - bearerAuth(tokenProvider.refreshToken) - } - .body() - tokenProvider.update(tokenResponse.toModel()) - throw ErrorRepeatEnd - } - } - } - - private val refreshTokenValidator: suspend (Throwable, HttpRequest) -> Unit - get() = { exception, _ -> - val clientException = exception as? ResponseException ?: throw exception - if (HttpStatusCode.Unauthorized.value == clientException.response.status.value) { - throw ErrorRefresh - } else { - throw clientException - } - } - - companion object { - private const val API_KEY_NAME = "X-Api-Key" - } -} - -/** - * Error repeat request. - * Show that the request was repeated after a refresh token - * @see ServerApiClientImpl.request - */ -private data object ErrorRepeatEnd : Throwable() - -/** - * Error refresh token response - * @see ServerApiClientImpl.refreshToken - * @see ServerApiClientImpl.errorParser - * @see ServerApiClientImpl.request - */ -data object ErrorRefresh : Throwable() diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClient.kt new file mode 100644 index 00000000..5130d573 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClient.kt @@ -0,0 +1,5 @@ +package com.stslex.core.network.api.server.client + +import com.stslex.core.network.api.base.NetworkClient + +interface ServerApiClient : NetworkClient diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt new file mode 100644 index 00000000..8e8c6817 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/client/ServerApiClientImpl.kt @@ -0,0 +1,23 @@ +package com.stslex.core.network.api.server.client + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.network.api.server.http_client.ServerHttpClient +import com.stslex.core.network.api.server.model.ErrorRepeatEnd +import io.ktor.client.HttpClient +import kotlinx.coroutines.withContext + +class ServerApiClientImpl( + private val appDispatcher: AppDispatcher, + private val client: ServerHttpClient +) : ServerApiClient { + + override suspend fun request( + request: suspend HttpClient.() -> T + ): T = withContext(appDispatcher.io) { + try { + request(client.authClient) + } catch (error: ErrorRepeatEnd) { + request(client.authClient) + } + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandler.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandler.kt new file mode 100644 index 00000000..17a76614 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandler.kt @@ -0,0 +1,5 @@ +package com.stslex.core.network.api.server.error_handler + +import io.ktor.client.plugins.CallRequestExceptionHandler + +interface ErrorHandler : CallRequestExceptionHandler \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandlerImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandlerImpl.kt new file mode 100644 index 00000000..0a62ac63 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/ErrorHandlerImpl.kt @@ -0,0 +1,52 @@ +package com.stslex.core.network.api.server.error_handler + +import com.stslex.core.network.api.server.error_handler.RefreshTokenValidator.setupResponseValidator +import com.stslex.core.network.api.server.http_client.ServerHttpClient +import com.stslex.core.network.api.server.model.ErrorRepeatEnd +import com.stslex.core.network.api.server.model.TokenResponseModel +import com.stslex.core.network.utils.token.AuthController +import com.stslex.core.network.utils.token.toModel +import io.ktor.client.call.body +import io.ktor.client.plugins.ResponseException +import io.ktor.client.request.HttpRequest +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +class ErrorHandlerImpl( + private val client: Lazy, + private val tokenProvider: AuthController, +) : ErrorHandler { + + private var refreshJob: Job? = null + + override suspend fun invoke(cause: Throwable, request: HttpRequest) { + when { + cause !is ResponseException -> throw cause + cause.response.status.value == HttpStatusCode.Unauthorized.value -> refreshToken() + else -> throw cause + } + } + + private suspend fun refreshToken() { + if (refreshJob?.isActive == true) return + refreshJob = coroutineScope { + launch { + val tokenResponse = client + .value + .client + .setupResponseValidator() + .get("passport/refresh") { + bearerAuth(tokenProvider.refreshToken) + } + .body() + tokenProvider.update(tokenResponse.toModel()) + // TODO remove error throw after refresh token (move logic out from throwing) + throw ErrorRepeatEnd + } + } + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/RefreshTokenValidator.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/RefreshTokenValidator.kt new file mode 100644 index 00000000..7592acf5 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/error_handler/RefreshTokenValidator.kt @@ -0,0 +1,27 @@ +package com.stslex.core.network.api.server.error_handler + +import com.stslex.core.network.api.server.model.ErrorRefresh +import io.ktor.client.HttpClient +import io.ktor.client.plugins.CallRequestExceptionHandler +import io.ktor.client.plugins.HttpResponseValidator +import io.ktor.client.plugins.ResponseException +import io.ktor.client.request.HttpRequest +import io.ktor.http.HttpStatusCode + +internal object RefreshTokenValidator : CallRequestExceptionHandler { + + fun HttpClient.setupResponseValidator(): HttpClient = config { + HttpResponseValidator { + handleResponseExceptionWithRequest(this@RefreshTokenValidator) + } + } + + override suspend fun invoke(cause: Throwable, request: HttpRequest) { + throw when { + cause !is ResponseException -> cause + cause.response.status.value == HttpStatusCode.Unauthorized.value -> ErrorRefresh + else -> cause + } + } +} + diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClient.kt new file mode 100644 index 00000000..5e859071 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClient.kt @@ -0,0 +1,10 @@ +package com.stslex.core.network.api.server.http_client + +import io.ktor.client.HttpClient + +interface ServerHttpClient { + + val client: HttpClient + + val authClient: HttpClient +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClientImpl.kt new file mode 100644 index 00000000..b26c3a5e --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/http_client/ServerHttpClientImpl.kt @@ -0,0 +1,65 @@ +package com.stslex.core.network.api.server.http_client + +import Wizard.core.network.BuildConfig +import com.stslex.core.network.api.base.NetworkClientBuilder.setupLogging +import com.stslex.core.network.api.base.NetworkClientBuilder.setupNegotiation +import com.stslex.core.network.api.server.error_handler.ErrorHandler +import com.stslex.core.network.utils.token.AuthController +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpResponseValidator +import io.ktor.client.plugins.auth.Auth +import io.ktor.client.plugins.auth.providers.BearerTokens +import io.ktor.client.plugins.auth.providers.bearer +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.headers +import io.ktor.http.ContentType +import io.ktor.http.URLProtocol +import io.ktor.http.contentType + +class ServerHttpClientImpl( + private val errorHandler: ErrorHandler, + private val tokenProvider: AuthController +) : ServerHttpClient { + + override val client: HttpClient = HttpClient(CIO) { + setupNegotiation() + setupLogging() + expectSuccess = true + HttpResponseValidator { + handleResponseExceptionWithRequest(errorHandler) + } + defaultRequest { + url( + scheme = URLProtocol.HTTP.name, + host = BuildConfig.SERVER_HOST, + port = BuildConfig.SERVER_PORT.toInt(), + path = BuildConfig.SERVER_API_VERSION, + block = { + contentType(ContentType.Application.Json) + } + ) + headers { + append(API_KEY_NAME, BuildConfig.SERVER_API_KEY) + } + } + } + + override val authClient: HttpClient + get() = client.config { + install(Auth) { + bearer { + loadTokens { + BearerTokens( + accessToken = tokenProvider.accessToken, + refreshToken = tokenProvider.refreshToken + ) + } + } + } + } + + companion object { + private const val API_KEY_NAME = "X-Api-Key" + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRefresh.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRefresh.kt new file mode 100644 index 00000000..41dc73f2 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRefresh.kt @@ -0,0 +1,9 @@ +package com.stslex.core.network.api.server.model + +/** + * Error refresh token response + * @see ServerApiClientImpl.refreshToken + * @see ServerApiClientImpl.errorParser + * @see ServerApiClientImpl.request + */ +data object ErrorRefresh : Throwable() \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRepeatEnd.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRepeatEnd.kt new file mode 100644 index 00000000..8b620246 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/ErrorRepeatEnd.kt @@ -0,0 +1,8 @@ +package com.stslex.core.network.api.server.model + +/** + * Error repeat request. + * Show that the request was repeated after a refresh token + * @see ServerApiClientImpl.request + */ +internal data object ErrorRepeatEnd : Throwable() \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/TokenResponseModel.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/TokenResponseModel.kt similarity index 87% rename from core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/TokenResponseModel.kt rename to core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/TokenResponseModel.kt index 09a60825..e9f47de8 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/TokenResponseModel.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/api/server/model/TokenResponseModel.kt @@ -1,4 +1,4 @@ -package com.stslex.core.network.api.server +package com.stslex.core.network.api.server.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/auth/client/AuthClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/auth/client/AuthClientImpl.kt index 566d00c0..92da443a 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/auth/client/AuthClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/auth/client/AuthClientImpl.kt @@ -1,6 +1,6 @@ package com.stslex.core.network.clients.auth.client -import com.stslex.core.network.api.server.ServerApiClient +import com.stslex.core.network.api.server.client.ServerApiClient import com.stslex.core.network.clients.auth.request.LoginRequest import com.stslex.core.network.clients.auth.request.RegisterRequest import com.stslex.core.network.clients.auth.response.LoginOkResponse diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt index fc5e9127..c92393b0 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt @@ -1,6 +1,6 @@ package com.stslex.core.network.clients.profile.client -import com.stslex.core.network.api.server.ServerApiClient +import com.stslex.core.network.api.server.client.ServerApiClient import com.stslex.core.network.clients.profile.model.ProfileResponse import io.ktor.client.call.body import io.ktor.client.request.get diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt index 83b54427..fc102b7c 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt @@ -4,8 +4,12 @@ import com.stslex.core.network.api.kinopoisk.api.KinopoiskApiClient import com.stslex.core.network.api.kinopoisk.api.KinopoiskApiClientImpl import com.stslex.core.network.api.kinopoisk.source.KinopoiskNetworkClient import com.stslex.core.network.api.kinopoisk.source.KinopoiskNetworkClientImpl -import com.stslex.core.network.api.server.ServerApiClient -import com.stslex.core.network.api.server.ServerApiClientImpl +import com.stslex.core.network.api.server.client.ServerApiClient +import com.stslex.core.network.api.server.client.ServerApiClientImpl +import com.stslex.core.network.api.server.error_handler.ErrorHandler +import com.stslex.core.network.api.server.error_handler.ErrorHandlerImpl +import com.stslex.core.network.api.server.http_client.ServerHttpClient +import com.stslex.core.network.api.server.http_client.ServerHttpClientImpl import com.stslex.core.network.clients.auth.client.AuthClient import com.stslex.core.network.clients.auth.client.AuthClientImpl import com.stslex.core.network.clients.film.client.FilmClient @@ -17,6 +21,21 @@ import com.stslex.core.network.utils.token.AuthControllerImpl import org.koin.dsl.module val coreNetworkModule = module { + /*Clients*/ + single { + ErrorHandlerImpl( + client = lazy { get() }, + tokenProvider = get() + ) + } + + single { + ServerHttpClientImpl( + errorHandler = get(), + tokenProvider = get() + ) + } + /*Kinopoisk Api*/ single { KinopoiskApiClientImpl(appDispatcher = get()) } single { KinopoiskNetworkClientImpl(apiClient = get()) } @@ -25,7 +44,7 @@ val coreNetworkModule = module { single { ServerApiClientImpl( appDispatcher = get(), - tokenProvider = get() + client = get() ) } diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/token/TokenModel.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/token/TokenModel.kt index f04bd8fe..44abe1c1 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/token/TokenModel.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/token/TokenModel.kt @@ -1,6 +1,6 @@ package com.stslex.core.network.utils.token -import com.stslex.core.network.api.server.TokenResponseModel +import com.stslex.core.network.api.server.model.TokenResponseModel import com.stslex.core.network.clients.auth.response.LoginOkResponse data class TokenModel( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt index 2174ad85..d0e39445 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel -import com.stslex.core.network.api.server.ErrorRefresh +import com.stslex.core.network.api.server.model.ErrorRefresh import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.profile.ui.store.ProfileScreenState import com.stslex.feature.profile.ui.store.ProfileStore