rewrite authentication flow

This commit is contained in:
2026-08-12 15:42:41 +03:00
parent 70c64a9251
commit d6a0d058ed
9 changed files with 109 additions and 20 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
alias(libs.plugins.convention.ktor.client)
}
val appVersion = "1.25.4"
val appVersion = "1.25.5"
android {
buildFeatures { buildConfig = true }
@@ -3,20 +3,20 @@ package ru.shadowsparky.vbox.shared.data
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import org.koin.core.annotation.Factory
import ru.shadowsparky.vbox.shared.domain.HeathCheck
import ru.shadowsparky.vbox.shared.domain.HealthCheck
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
@Factory
class HeathCheckImpl(
class HealthCheckImpl(
private val httpClient: HttpClient,
private val serverConfigurationProvider: ServerConfigurationRepository,
) : HeathCheck {
) : HealthCheck {
override suspend fun check() {
httpClient.get {
serverConfigurationProvider.getServerConfiguration()
.apply(this, HeathCheck.PATH)
.apply(this, HealthCheck.PATH)
}
}
}
@@ -9,18 +9,24 @@ import io.ktor.websocket.readText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Single
import ru.shadowsparky.domain.Log
import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.vbox.shared.domain.AuthRequest
import ru.shadowsparky.vbox.shared.domain.AuthResponse
import ru.shadowsparky.vbox.shared.domain.HealthCheck
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
@@ -34,21 +40,22 @@ class RemoteEventListenerImpl(
private val httpClient: HttpClient,
private val serverConfigurationRepository: ServerConfigurationRepository,
private val authTokenCache: TokenStorage,
private val healthCheck: HealthCheck,
private val json: Json,
private val log: Log
) : RemoteEventListener {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override val event = flow {
@OptIn(ExperimentalCoroutinesApi::class)
override val event = authTokenCache.token.filterNotNull().transformLatest { token ->
val config = serverConfigurationRepository.getServerConfiguration()
val token = authTokenCache.token.first() ?: return@flow
log.d(TAG, "listener started")
try {
httpClient.webSocket(
method = HttpMethod.Get,
request = { url(config.asWebSocketStr() + "/${RemoteEventHandler.ON_EVENT}") }
) {
outgoing.send(Frame.Text(token.token))
authV2(token.token, incoming, outgoing)
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
@@ -70,6 +77,20 @@ class RemoteEventListenerImpl(
}
}.retryExponential { true }.shareIn(scope, SharingStarted.WhileSubscribed(500), 0)
private suspend fun authV2(
token: String,
input: ReceiveChannel<Frame>,
output: SendChannel<Frame>
) {
output.send(Frame.Text(json.encodeToString(AuthRequest(token))))
val rawFrame = (input.receive() as Frame.Text).readText()
val response = json.decodeFromString<AuthResponse>(rawFrame)
if (!response.ok) {
healthCheck.check()
error("Authentication failed")
}
}
private fun <T> Flow<T>.retryExponential(
maxRetries: Int = Int.MAX_VALUE,
initialDelay: Long = 5000L,
@@ -19,7 +19,7 @@ import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.ui.nav.RootComponent
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider
import ru.shadowsparky.vbox.shared.domain.HeathCheck
import ru.shadowsparky.vbox.shared.domain.HealthCheck
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
import ru.shadowsparky.vbox.shared.presentation.nav.Route
@@ -28,7 +28,7 @@ class VBoxRootComponent(
componentContext: ComponentContext,
tokenStorage: TokenStorage,
private val initRoute: Route,
private val healthCheck: HeathCheck,
private val healthCheck: HealthCheck,
val imageLoaderProvider: ImageLoaderProvider,
updateComponentFactory: UpdateComponentFactory,
private val remoteEventListener: RemoteEventListener,
@@ -91,7 +91,7 @@ class VBoxRootComponent(
@Factory
class RootFactory(
private val tokenStorage: TokenStorage,
private val heathCheck: HeathCheck,
private val healthCheck: HealthCheck,
private val imageLoaderProvider: ImageLoaderProvider,
private val updateComponentFactory: UpdateComponentFactory,
private val remoteEventListener: RemoteEventListener
@@ -111,7 +111,7 @@ class RootFactory(
context,
tokenStorage,
Route.Loading(next = initStack.toSet()),
heathCheck,
healthCheck,
imageLoaderProvider,
updateComponentFactory,
remoteEventListener