Skip to content

Commit

Permalink
Merge pull request #24 from stslex/dev
Browse files Browse the repository at this point in the history
dev
  • Loading branch information
stslex authored Jan 13, 2024
2 parents 74bcf54 + c0f1972 commit f4c0adc
Show file tree
Hide file tree
Showing 28 changed files with 418 additions and 70 deletions.
1 change: 1 addition & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"
Expand Down
17 changes: 17 additions & 0 deletions composeApp/src/androidMain/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<!-- Trust user added CAs while debuggable only -->
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>

<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
4 changes: 4 additions & 0 deletions core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ fun BuildConfigExtension.setLocalProperty(dir: Project) {
val serverPort = gradleLocalProperties(dir.projectDir)["SERVER_PORT"]
?.toString()
?: throw IllegalStateException("SERVER_PORT should be initialised")
val serverApiKey = gradleLocalProperties(dir.projectDir)["SERVER_API_KEY"]
?.toString()
?: throw IllegalStateException("SERVER_API_KEY should be initialised")

buildConfigField("String", "KINOPOISK_API_KEY", key)
buildConfigField("String", "SERVER_HOST", serverHost)
buildConfigField("String", "SERVER_API_VERSION", serverApiVersion)
buildConfigField("String", "SERVER_PORT", serverPort)
buildConfigField("String", "SERVER_API_KEY", serverApiKey)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ 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
Expand All @@ -32,22 +36,12 @@ class ServerApiClientImpl(
private val appDispatcher: AppDispatcher
) : ServerApiClient {

private val client = HttpClient(CIO) {
setupNegotiation()
private var refreshJob: Job? = null

val client = HttpClient(CIO) {
setupNegotiation()
setupLogging()
expectSuccess = true

install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = tokenProvider.accessToken,
refreshToken = tokenProvider.refreshToken
)
}
}
}
HttpResponseValidator {
handleResponseExceptionWithRequest(errorParser)
}
Expand All @@ -62,15 +56,33 @@ class ServerApiClientImpl(
}
)
headers {
append(API_KEY_NAME, "API_KEY") // TODO move to buildConfig
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 <T> request(
request: suspend HttpClient.() -> T
): T = withContext(appDispatcher.io) {
request(client)
try {
request(authClient)
} catch (error: ErrorRepeatEnd) {
request(authClient)
}
}

private val errorParser: suspend (Throwable, HttpRequest) -> Unit
Expand All @@ -84,13 +96,51 @@ class ServerApiClientImpl(
}

private suspend fun refreshToken() {
val tokenResponse = client
.get("passport/refresh")
.body<TokenResponseModel>()
tokenProvider.update(tokenResponse.toModel())
if (refreshJob?.isActive == true) return
refreshJob = coroutineScope {
launch {
val tokenResponse = client
.config {
HttpResponseValidator {
handleResponseExceptionWithRequest(refreshTokenValidator)
}
}
.get("passport/refresh") {
bearerAuth(tokenProvider.refreshToken)
}
.body<TokenResponseModel>()
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"
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()
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ class MockProfileClientImpl : ProfileClient {
favouriteCount = 873
)
}

override suspend fun getProfile(): ProfileResponse {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ import com.stslex.core.network.clients.profile.model.ProfileResponse
interface ProfileClient {

suspend fun getProfile(uuid: String): ProfileResponse

suspend fun getProfile(): ProfileResponse
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package com.stslex.core.network.clients.profile.client

import com.stslex.core.network.api.base.NetworkClient
import com.stslex.core.network.api.server.ServerApiClient
import com.stslex.core.network.clients.profile.model.ProfileResponse
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter

class ProfileClientImpl(
private val client: NetworkClient
private val client: ServerApiClient
) : ProfileClient {

override suspend fun getProfile(
uuid: String
): ProfileResponse = client.request {
get("profile") {
get(HOST){
parameter("uuid", uuid)
}.body()
}

override suspend fun getProfile(): ProfileResponse = client.request {
get(HOST).body()
}

companion object {
private const val HOST = "user"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ data class ProfileResponse(
@SerialName("username")
val username: String,
@SerialName("avatar_url")
val avatarUrl: String,
val avatarUrl: String = "",
@SerialName("bio")
val bio: String,
val bio: String = "",
@SerialName("followers")
val followers: Int,
val followers: Int = 0,
@SerialName("following")
val following: Int,
val following: Int = 0,
@SerialName("favourite_count")
val favouriteCount: Int
val favouriteCount: Int = 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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
import com.stslex.core.network.clients.film.client.MockFilmClientImpl
import com.stslex.core.network.clients.profile.client.MockProfileClientImpl
import com.stslex.core.network.clients.profile.client.ProfileClient
import com.stslex.core.network.clients.profile.client.ProfileClientImpl
import com.stslex.core.network.utils.token.AuthController
import com.stslex.core.network.utils.token.AuthControllerImpl
import org.koin.dsl.module
Expand Down Expand Up @@ -43,7 +43,9 @@ val coreNetworkModule = module {
// kinopoiskClient = get()
// )
}
single<ProfileClient> { MockProfileClientImpl() }
single<ProfileClient> {
ProfileClientImpl(client = get())
}

/*Utils*/
single<AuthController> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ interface AuthController {
val accessToken: String
val refreshToken: String

fun update(token: TokenModel)
suspend fun update(token: TokenModel)
suspend fun logOut()
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class AuthControllerImpl(
private val userStore: UserStore
) : AuthController {

init {
userStore.accessToken = "123"
}

override val isAuth: Boolean
get() = userStore.accessToken.isNotEmpty()

Expand All @@ -21,11 +25,16 @@ class AuthControllerImpl(
override val refreshToken: String
get() = userStore.refreshToken

override fun update(token: TokenModel) {
override suspend fun update(token: TokenModel) {
userStore.accessToken = token.accessToken
userStore.refreshToken = token.refreshToken
userStore.username = token.username
userStore.uuid = token.uuid
_isAuthFlow.value = token.accessToken.isNotEmpty()
_isAuthFlow.emit(token.accessToken.isNotEmpty())
}

override suspend fun logOut() {
userStore.clear()
_isAuthFlow.emit(false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.stslex.core.ui.components

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Row
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Snackbar
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AppSnackbar(
type: SnackbarType,
modifier: Modifier = Modifier,
) {
val swipeableState = rememberSwipeableState(SnackbarSwipeState.NONE)
var width by remember { mutableStateOf(0f) }

Snackbar(
modifier = modifier
.swipeable(
state = swipeableState,
orientation = Orientation.Horizontal,
anchors = mapOf(
0f to SnackbarSwipeState.LEFT,
width * 0.5f to SnackbarSwipeState.RIGHT,
width to SnackbarSwipeState.RIGHT
),
)
.onGloballyPositioned {
width = it.size.width.toFloat()
},
action = {
Text(text = "OK")
}
) {
Row {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "info"
)
Text(
modifier = Modifier,
text = type.message,
color = MaterialTheme.colorScheme.onBackground
)
}
}
}

enum class SnackbarSwipeState {
LEFT,
RIGHT,
NONE
}

sealed class SnackbarType(
val message: String
) {
data class Error(
private val mes: String
) : SnackbarType(mes)

data class Success(
private val mes: String
) : SnackbarType(mes)

data class Info(
private val mes: String
) : SnackbarType(mes)
}
Loading

0 comments on commit f4c0adc

Please sign in to comment.