mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Initial auth support(ws currently broken, bad login dialog)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
object Config {
|
||||
const val migrationCode = 5
|
||||
const val migrationCode = 6
|
||||
|
||||
// Suwayomi-Server version
|
||||
const val tachideskVersion = "v2.1.1959"
|
||||
|
||||
12
data/src/commonMain/graphql/User.graphql
Normal file
12
data/src/commonMain/graphql/User.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
mutation Login( $username: String!, $password: String!) {
|
||||
login(input: {username: $username, password: $password}) {
|
||||
refreshToken
|
||||
accessToken
|
||||
}
|
||||
}
|
||||
|
||||
mutation RefreshAccessToken($refreshToken: String!) {
|
||||
refreshToken(input: {refreshToken: $refreshToken}) {
|
||||
accessToken
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import ca.gosyer.jui.data.manga.MangaRepositoryImpl
|
||||
import ca.gosyer.jui.data.settings.SettingsRepositoryImpl
|
||||
import ca.gosyer.jui.data.source.SourceRepositoryImpl
|
||||
import ca.gosyer.jui.data.updates.UpdatesRepositoryImpl
|
||||
import ca.gosyer.jui.data.user.UserRepositoryImpl
|
||||
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||
import ca.gosyer.jui.domain.category.service.CategoryRepository
|
||||
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
|
||||
@@ -27,12 +28,13 @@ import ca.gosyer.jui.domain.global.service.GlobalRepository
|
||||
import ca.gosyer.jui.domain.library.service.LibraryRepository
|
||||
import ca.gosyer.jui.domain.manga.service.MangaRepository
|
||||
import ca.gosyer.jui.domain.server.Http
|
||||
import ca.gosyer.jui.domain.server.HttpNoAuth
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.settings.service.SettingsRepository
|
||||
import ca.gosyer.jui.domain.source.service.SourceRepository
|
||||
import ca.gosyer.jui.domain.updates.service.UpdatesRepository
|
||||
import ca.gosyer.jui.domain.user.service.UserRepository
|
||||
import com.apollographql.apollo.ApolloClient
|
||||
import com.apollographql.apollo.annotations.ApolloExperimental
|
||||
import com.apollographql.apollo.network.ws.GraphQLWsProtocol
|
||||
import com.apollographql.ktor.ktorClient
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -50,6 +52,8 @@ import me.tatarka.inject.annotations.Provides
|
||||
|
||||
typealias ApolloAppClient = StateFlow<ApolloClient>
|
||||
|
||||
typealias ApolloAppClientNoAuth = StateFlow<ApolloClient>
|
||||
|
||||
private fun getApolloClient(
|
||||
httpClient: HttpClient,
|
||||
serverUrl: Url,
|
||||
@@ -66,7 +70,7 @@ private fun getApolloClient(
|
||||
}
|
||||
|
||||
interface DataComponent : SharedDataComponent {
|
||||
@OptIn(ApolloExperimental::class)
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun apolloAppClient(
|
||||
@@ -81,6 +85,20 @@ interface DataComponent : SharedDataComponent {
|
||||
getApolloClient(http.value, serverPreferences.serverUrl().get()),
|
||||
)
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun apolloAppClientNoAuth(
|
||||
httpNoAuth: HttpNoAuth,
|
||||
serverPreferences: ServerPreferences,
|
||||
): ApolloAppClientNoAuth =
|
||||
httpNoAuth
|
||||
.map { getApolloClient(it, serverPreferences.serverUrl().get()) }
|
||||
.stateIn(
|
||||
GlobalScope,
|
||||
SharingStarted.Eagerly,
|
||||
getApolloClient(httpNoAuth.value, serverPreferences.serverUrl().get()),
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun settingsRepository(apolloAppClient: ApolloAppClient): SettingsRepository = SettingsRepositoryImpl(apolloAppClient)
|
||||
|
||||
@@ -147,6 +165,15 @@ interface DataComponent : SharedDataComponent {
|
||||
serverPreferences: ServerPreferences,
|
||||
): UpdatesRepository = UpdatesRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||
|
||||
@Provides
|
||||
fun userRepository(
|
||||
apolloAppClient: ApolloAppClient,
|
||||
apolloAppClientNoAuth: ApolloAppClientNoAuth,
|
||||
http: Http,
|
||||
httpNoAuth: HttpNoAuth,
|
||||
serverPreferences: ServerPreferences,
|
||||
): UserRepository = UserRepositoryImpl(apolloAppClient, apolloAppClientNoAuth, http, httpNoAuth,serverPreferences.serverUrl().get())
|
||||
|
||||
@Provides
|
||||
fun backupRepository(
|
||||
apolloAppClient: ApolloAppClient,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.data.user
|
||||
|
||||
import ca.gosyer.jui.data.ApolloAppClient
|
||||
import ca.gosyer.jui.data.ApolloAppClientNoAuth
|
||||
import ca.gosyer.jui.data.graphql.LoginMutation
|
||||
import ca.gosyer.jui.data.graphql.RefreshAccessTokenMutation
|
||||
import ca.gosyer.jui.domain.server.Http
|
||||
import ca.gosyer.jui.domain.server.HttpNoAuth
|
||||
import ca.gosyer.jui.domain.user.model.LoginData
|
||||
import ca.gosyer.jui.domain.user.service.UserRepository
|
||||
import com.apollographql.apollo.ApolloClient
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class UserRepositoryImpl(
|
||||
private val apolloAppClient: ApolloAppClient,
|
||||
private val apolloAppClientNoAuth: ApolloAppClientNoAuth,
|
||||
private val http: Http,
|
||||
private val httpNoAuth: HttpNoAuth,
|
||||
private val serverUrl: Url,
|
||||
) : UserRepository {
|
||||
val apolloClient: ApolloClient
|
||||
get() = apolloAppClient.value
|
||||
|
||||
val apolloClientNoAuth: ApolloClient
|
||||
get() = apolloAppClientNoAuth.value
|
||||
|
||||
override fun loginUI(username: String, password: String): Flow<LoginData> {
|
||||
return apolloClientNoAuth.mutation(
|
||||
LoginMutation(username, password),
|
||||
)
|
||||
.toFlow()
|
||||
.map {
|
||||
val data = it.dataAssertNoErrors.login
|
||||
LoginData(data.refreshToken, data.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshUI(refreshToken: String): Flow<String> {
|
||||
return apolloClientNoAuth.mutation(
|
||||
RefreshAccessTokenMutation(refreshToken),
|
||||
)
|
||||
.toFlow()
|
||||
.map {
|
||||
val data = it.dataAssertNoErrors.refreshToken
|
||||
data.accessToken
|
||||
}
|
||||
}
|
||||
|
||||
override fun loginSimple(username: String, password: String): Flow<String> {
|
||||
return flow {
|
||||
val cookie = httpNoAuth.value.post(
|
||||
"/login.html",
|
||||
) {
|
||||
formData {
|
||||
append("user", username)
|
||||
append("pass", password)
|
||||
}
|
||||
}.headers["Set-Cookie"]
|
||||
|
||||
cookie ?: throw NullPointerException("Missing Set-Cookie header")
|
||||
|
||||
emit(cookie.substringAfter("JSESSIONID=").substringBefore(";"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ abstract class AppComponent(
|
||||
@get:AppScope
|
||||
@get:Provides
|
||||
protected val appMigrationsFactory: AppMigrations
|
||||
get() = AppMigrations(migrationPreferences, contextWrapper)
|
||||
get() = AppMigrations(migrationPreferences, serverHostPreferences, contextWrapper)
|
||||
|
||||
val bind: ViewModelComponent
|
||||
@Provides get() = this
|
||||
|
||||
@@ -9,6 +9,8 @@ package ca.gosyer.jui.desktop
|
||||
import ca.gosyer.appdirs.AppDirs
|
||||
import ca.gosyer.jui.desktop.build.BuildConfig
|
||||
import ca.gosyer.jui.domain.migration.service.MigrationPreferences
|
||||
import ca.gosyer.jui.domain.server.service.ServerHostPreferences
|
||||
import ca.gosyer.jui.domain.settings.model.AuthMode
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import com.diamondedge.logging.logging
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
@@ -18,6 +20,7 @@ import okio.Path.Companion.toPath
|
||||
@Inject
|
||||
class AppMigrations(
|
||||
private val migrationPreferences: MigrationPreferences,
|
||||
private val serverHostPreference: ServerHostPreferences,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
@@ -32,8 +35,8 @@ class AppMigrations(
|
||||
}
|
||||
|
||||
if (oldVersion < 5) {
|
||||
val oldDir = AppDirs("Tachidesk-JUI").getUserDataDir().toPath()
|
||||
val newDir = AppDirs("Suwayomi-JUI").getUserDataDir().toPath()
|
||||
val oldDir = AppDirs { appName = "Tachidesk-JUI" }.getUserDataDir().toPath()
|
||||
val newDir = AppDirs { appName = "Suwayomi-JUI" }.getUserDataDir().toPath()
|
||||
try {
|
||||
FileSystem.SYSTEM.list(oldDir)
|
||||
.filter { FileSystem.SYSTEM.metadata(it).isDirectory }
|
||||
@@ -48,6 +51,14 @@ class AppMigrations(
|
||||
log.e(e) { "Failed to run directory migration" }
|
||||
}
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
val basicAuthEnabled = serverHostPreference.basicAuthEnabled().get()
|
||||
if (basicAuthEnabled) {
|
||||
serverHostPreference.authMode().set(AuthMode.BASIC_AUTH)
|
||||
}
|
||||
serverHostPreference.authUsername().set(serverHostPreference.basicAuthUsername().get())
|
||||
serverHostPreference.authPassword().set(serverHostPreference.basicAuthPassword().get())
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -16,13 +16,17 @@ import ca.gosyer.jui.domain.migration.interactor.RunMigrations
|
||||
import ca.gosyer.jui.domain.migration.service.MigrationPreferences
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.domain.server.Http
|
||||
import ca.gosyer.jui.domain.server.HttpNoAuth
|
||||
import ca.gosyer.jui.domain.server.httpClient
|
||||
import ca.gosyer.jui.domain.server.httpClientNoAuth
|
||||
import ca.gosyer.jui.domain.server.service.ServerHostPreferences
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
||||
import ca.gosyer.jui.domain.ui.service.UiPreferences
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateChecker
|
||||
import ca.gosyer.jui.domain.updates.service.UpdatePreferences
|
||||
import ca.gosyer.jui.domain.user.interactor.UserRefreshUI
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import kotlinx.serialization.json.Json
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
@@ -39,6 +43,8 @@ interface SharedDomainComponent : CoreComponent {
|
||||
|
||||
val http: Http
|
||||
|
||||
val httpNoAuth: HttpNoAuth
|
||||
|
||||
val serverPreferences: ServerPreferences
|
||||
|
||||
val extensionPreferences: ExtensionPreferences
|
||||
@@ -59,12 +65,30 @@ interface SharedDomainComponent : CoreComponent {
|
||||
|
||||
val json: Json
|
||||
|
||||
val userRefreshUI: UserRefreshUI
|
||||
|
||||
@get:AppScope
|
||||
@get:Provides
|
||||
val lazyUserRefreshUIFactory: Lazy<UserRefreshUI>
|
||||
get() = lazy { userRefreshUI }
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
fun httpFactory(
|
||||
serverPreferences: ServerPreferences,
|
||||
userPreferences: UserPreferences,
|
||||
userRefreshUI: Lazy<UserRefreshUI>,
|
||||
json: Json,
|
||||
) = httpClient(serverPreferences, json)
|
||||
): Http = httpClient(serverPreferences, userPreferences, userRefreshUI, json)
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
fun httpNoAuthFactory(
|
||||
serverPreferences: ServerPreferences,
|
||||
userPreferences: UserPreferences,
|
||||
userRefreshUI: Lazy<UserRefreshUI>,
|
||||
json: Json,
|
||||
): HttpNoAuth = httpClientNoAuth(serverPreferences, userPreferences, userRefreshUI, json)
|
||||
|
||||
@get:AppScope
|
||||
@get:Provides
|
||||
@@ -108,6 +132,11 @@ interface SharedDomainComponent : CoreComponent {
|
||||
val updatePreferencesFactory: UpdatePreferences
|
||||
get() = UpdatePreferences(preferenceFactory.create("update"))
|
||||
|
||||
@get:AppScope
|
||||
@get:Provides
|
||||
val userPreferencesFactory: UserPreferences
|
||||
get() = UserPreferences(preferenceFactory.create("user"))
|
||||
|
||||
@get:AppScope
|
||||
@get:Provides
|
||||
val serverHostPreferencesFactory: ServerHostPreferences
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
package ca.gosyer.jui.domain.server
|
||||
|
||||
import ca.gosyer.jui.core.prefs.Preference
|
||||
import ca.gosyer.jui.domain.build.BuildKonfig
|
||||
import ca.gosyer.jui.domain.server.model.Auth
|
||||
import ca.gosyer.jui.domain.server.model.Proxy
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.user.interactor.UserRefreshUI
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import com.diamondedge.logging.logging
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
@@ -17,6 +20,8 @@ import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
import io.ktor.client.engine.ProxyBuilder
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.api.Send
|
||||
import io.ktor.client.plugins.api.createClientPlugin
|
||||
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
|
||||
import io.ktor.client.plugins.auth.providers.DigestAuthCredentials
|
||||
import io.ktor.client.plugins.auth.providers.basic
|
||||
@@ -27,6 +32,7 @@ import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.http.URLProtocol
|
||||
import io.ktor.http.Url
|
||||
@@ -44,10 +50,65 @@ import io.ktor.client.plugins.auth.Auth as AuthPlugin
|
||||
|
||||
typealias Http = StateFlow<HttpClient>
|
||||
|
||||
typealias HttpNoAuth = StateFlow<HttpClient>
|
||||
|
||||
expect val Engine: HttpClientEngineFactory<HttpClientEngineConfig>
|
||||
|
||||
expect fun HttpClientConfig<HttpClientEngineConfig>.configurePlatform()
|
||||
|
||||
private val log = logging()
|
||||
|
||||
private class SimpleAuthPluginConfig {
|
||||
var simpleSessionPreference: Preference<String>? = null
|
||||
}
|
||||
|
||||
private val SimpleAuthPlugin = createClientPlugin("SimpleAuthPlugin", ::SimpleAuthPluginConfig) {
|
||||
val simpleSessionPreference = pluginConfig.simpleSessionPreference!!
|
||||
|
||||
on(Send) { request ->
|
||||
val session = simpleSessionPreference.get()
|
||||
if (session.isNotEmpty()) {
|
||||
request.headers.append("Cookie", "JSESSIONID=$session")
|
||||
}
|
||||
proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
private class UIAuthPluginConfig {
|
||||
var uiAccessTokenPreference: Preference<String>? = null
|
||||
var userRefreshUI: Lazy<UserRefreshUI>? = null
|
||||
}
|
||||
|
||||
private val UIAuthPlugin = createClientPlugin("UIAuthPlugin", ::UIAuthPluginConfig) {
|
||||
val uiAccessTokenPreference = pluginConfig.uiAccessTokenPreference!!
|
||||
val userRefreshUI = pluginConfig.userRefreshUI!!
|
||||
|
||||
on(Send) { request ->
|
||||
val token = uiAccessTokenPreference.get()
|
||||
if (token.isNotEmpty()) {
|
||||
request.headers.append("Authorization", "Bearer $token")
|
||||
val originalCall = proceed(request)
|
||||
if (originalCall.response.status == HttpStatusCode.Unauthorized) {
|
||||
log.warn { "Token expired, refreshing..." }
|
||||
val newToken = userRefreshUI.value.await()
|
||||
if (newToken != null) {
|
||||
request.headers.remove("Authorization")
|
||||
request.headers.append("Authorization", "Bearer $newToken")
|
||||
proceed(request)
|
||||
} else {
|
||||
originalCall
|
||||
}
|
||||
} else {
|
||||
originalCall
|
||||
}
|
||||
} else {
|
||||
proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun getHttpClient(
|
||||
serverUrl: Url,
|
||||
proxy: Proxy,
|
||||
@@ -58,6 +119,9 @@ private fun getHttpClient(
|
||||
auth: Auth,
|
||||
authUsername: String,
|
||||
authPassword: String,
|
||||
uiAccessTokenPreference: Preference<String>,
|
||||
simpleSessionPreference: Preference<String>,
|
||||
userRefreshUI: Lazy<UserRefreshUI>,
|
||||
json: Json
|
||||
): HttpClient {
|
||||
return HttpClient(Engine) {
|
||||
@@ -113,6 +177,13 @@ private fun getHttpClient(
|
||||
}
|
||||
}
|
||||
}
|
||||
Auth.SIMPLE -> install(SimpleAuthPlugin) {
|
||||
this.simpleSessionPreference = simpleSessionPreference
|
||||
}
|
||||
Auth.UI -> install(UIAuthPlugin) {
|
||||
this.uiAccessTokenPreference = uiAccessTokenPreference
|
||||
this.userRefreshUI = userRefreshUI
|
||||
}
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
connectTimeoutMillis = 30.seconds.inWholeMilliseconds
|
||||
@@ -143,6 +214,8 @@ private fun getHttpClient(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun httpClient(
|
||||
serverPreferences: ServerPreferences,
|
||||
userPreferences: UserPreferences,
|
||||
userRefreshUI: Lazy<UserRefreshUI>,
|
||||
json: Json,
|
||||
): Http = combine(
|
||||
serverPreferences.serverUrl().stateIn(GlobalScope),
|
||||
@@ -156,30 +229,85 @@ fun httpClient(
|
||||
serverPreferences.authPassword().stateIn(GlobalScope),
|
||||
) {
|
||||
getHttpClient(
|
||||
it[0] as Url,
|
||||
it[1] as Proxy,
|
||||
it[2] as String,
|
||||
it[3] as Int,
|
||||
it[4] as String,
|
||||
it[5] as Int,
|
||||
it[6] as Auth,
|
||||
it[7] as String,
|
||||
it[8] as String,
|
||||
json,
|
||||
serverUrl = it[0] as Url,
|
||||
proxy = it[1] as Proxy,
|
||||
proxyHttpHost = it[2] as String,
|
||||
proxyHttpPort = it[3] as Int,
|
||||
proxySocksHost = it[4] as String,
|
||||
proxySocksPort = it[5] as Int,
|
||||
auth = it[6] as Auth,
|
||||
authUsername = it[7] as String,
|
||||
authPassword = it[8] as String,
|
||||
uiAccessTokenPreference = userPreferences.uiAccessToken(),
|
||||
simpleSessionPreference = userPreferences.simpleSession(),
|
||||
userRefreshUI = userRefreshUI,
|
||||
json = json,
|
||||
)
|
||||
}.stateIn(
|
||||
GlobalScope,
|
||||
SharingStarted.Eagerly,
|
||||
getHttpClient(
|
||||
serverPreferences.serverUrl().get(),
|
||||
serverPreferences.proxy().get(),
|
||||
serverPreferences.proxyHttpHost().get(),
|
||||
serverPreferences.proxyHttpPort().get(),
|
||||
serverPreferences.proxySocksHost().get(),
|
||||
serverPreferences.proxySocksPort().get(),
|
||||
serverPreferences.auth().get(),
|
||||
serverPreferences.authUsername().get(),
|
||||
serverPreferences.authPassword().get(),
|
||||
json,
|
||||
serverUrl = serverPreferences.serverUrl().get(),
|
||||
proxy = serverPreferences.proxy().get(),
|
||||
proxyHttpHost = serverPreferences.proxyHttpHost().get(),
|
||||
proxyHttpPort = serverPreferences.proxyHttpPort().get(),
|
||||
proxySocksHost = serverPreferences.proxySocksHost().get(),
|
||||
proxySocksPort = serverPreferences.proxySocksPort().get(),
|
||||
auth = serverPreferences.auth().get(),
|
||||
authUsername = serverPreferences.authUsername().get(),
|
||||
authPassword = serverPreferences.authPassword().get(),
|
||||
uiAccessTokenPreference = userPreferences.uiAccessToken(),
|
||||
simpleSessionPreference = userPreferences.simpleSession(),
|
||||
userRefreshUI = userRefreshUI,
|
||||
json = json,
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun httpClientNoAuth(
|
||||
serverPreferences: ServerPreferences,
|
||||
userPreferences: UserPreferences,
|
||||
userRefreshUI: Lazy<UserRefreshUI>,
|
||||
json: Json,
|
||||
): Http = combine(
|
||||
serverPreferences.serverUrl().stateIn(GlobalScope),
|
||||
serverPreferences.proxy().stateIn(GlobalScope),
|
||||
serverPreferences.proxyHttpHost().stateIn(GlobalScope),
|
||||
serverPreferences.proxyHttpPort().stateIn(GlobalScope),
|
||||
serverPreferences.proxySocksHost().stateIn(GlobalScope),
|
||||
serverPreferences.proxySocksPort().stateIn(GlobalScope),
|
||||
) {
|
||||
getHttpClient(
|
||||
serverUrl = it[0] as Url,
|
||||
proxy = it[1] as Proxy,
|
||||
proxyHttpHost = it[2] as String,
|
||||
proxyHttpPort = it[3] as Int,
|
||||
proxySocksHost = it[4] as String,
|
||||
proxySocksPort = it[5] as Int,
|
||||
auth = Auth.NONE,
|
||||
authUsername = "",
|
||||
authPassword = "",
|
||||
uiAccessTokenPreference = userPreferences.uiAccessToken(),
|
||||
simpleSessionPreference = userPreferences.simpleSession(),
|
||||
userRefreshUI = userRefreshUI,
|
||||
json = json,
|
||||
)
|
||||
}.stateIn(
|
||||
GlobalScope,
|
||||
SharingStarted.Eagerly,
|
||||
getHttpClient(
|
||||
serverUrl = serverPreferences.serverUrl().get(),
|
||||
proxy = serverPreferences.proxy().get(),
|
||||
proxyHttpHost = serverPreferences.proxyHttpHost().get(),
|
||||
proxyHttpPort = serverPreferences.proxyHttpPort().get(),
|
||||
proxySocksHost = serverPreferences.proxySocksHost().get(),
|
||||
proxySocksPort = serverPreferences.proxySocksPort().get(),
|
||||
auth = Auth.NONE,
|
||||
authUsername = "",
|
||||
authPassword = "",
|
||||
uiAccessTokenPreference = userPreferences.uiAccessToken(),
|
||||
simpleSessionPreference = userPreferences.simpleSession(),
|
||||
userRefreshUI = userRefreshUI,
|
||||
json = json,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -15,4 +15,6 @@ enum class Auth {
|
||||
NONE,
|
||||
BASIC,
|
||||
DIGEST,
|
||||
SIMPLE,
|
||||
UI,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package ca.gosyer.jui.domain.user.interactor
|
||||
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import ca.gosyer.jui.domain.user.service.UserRepository
|
||||
import com.diamondedge.logging.logging
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
@Inject
|
||||
class UserLoginSimple(
|
||||
private val userRepository: UserRepository,
|
||||
private val userPreferences: UserPreferences,
|
||||
) {
|
||||
suspend fun await(
|
||||
username: String,
|
||||
password: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(username, password)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to login with user $username" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
username: String,
|
||||
password: String,
|
||||
) = userRepository.loginSimple(username, password)
|
||||
.onEach {
|
||||
userPreferences.simpleSession().set(it)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ca.gosyer.jui.domain.user.interactor
|
||||
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import ca.gosyer.jui.domain.user.service.UserRepository
|
||||
import com.diamondedge.logging.logging
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
@Inject
|
||||
class UserLoginUI(
|
||||
private val userRepository: UserRepository,
|
||||
private val userPreferences: UserPreferences,
|
||||
) {
|
||||
suspend fun await(
|
||||
username: String,
|
||||
password: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(username, password)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to login with user $username" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
username: String,
|
||||
password: String,
|
||||
) = userRepository.loginUI(username, password)
|
||||
.onEach {
|
||||
userPreferences.uiRefreshToken().set(it.refreshToken)
|
||||
userPreferences.uiAccessToken().set(it.accessToken)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ca.gosyer.jui.domain.user.interactor
|
||||
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import com.diamondedge.logging.logging
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
@Inject
|
||||
class UserLogout(
|
||||
private val userPreferences: UserPreferences,
|
||||
) {
|
||||
suspend fun await(
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to logout user" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = flow {
|
||||
userPreferences.uiRefreshToken().set("")
|
||||
userPreferences.uiAccessToken().set("")
|
||||
userPreferences.simpleSession().set("")
|
||||
emit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ca.gosyer.jui.domain.user.interactor
|
||||
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import ca.gosyer.jui.domain.user.service.UserRepository
|
||||
import com.diamondedge.logging.logging
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
@Inject
|
||||
class UserRefreshUI(
|
||||
private val userRepository: UserRepository,
|
||||
private val userPreferences: UserPreferences,
|
||||
) {
|
||||
suspend fun await(
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh user" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = userRepository.refreshUI(userPreferences.uiRefreshToken().get())
|
||||
.onEach {
|
||||
userPreferences.uiAccessToken().set(it)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ca.gosyer.jui.domain.user.model
|
||||
|
||||
data class LoginData(
|
||||
val refreshToken: String,
|
||||
val accessToken: String,
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package ca.gosyer.jui.domain.user.service
|
||||
|
||||
import ca.gosyer.jui.core.prefs.Preference
|
||||
import ca.gosyer.jui.core.prefs.PreferenceStore
|
||||
|
||||
class UserPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
fun uiRefreshToken(): Preference<String> = preferenceStore.getString("ui_refresh_token", "")
|
||||
|
||||
fun uiAccessToken(): Preference<String> = preferenceStore.getString("ui_refresh_token", "")
|
||||
|
||||
fun simpleSession(): Preference<String> = preferenceStore.getString("simple_session", "")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ca.gosyer.jui.domain.user.service
|
||||
|
||||
import ca.gosyer.jui.domain.user.model.LoginData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UserRepository {
|
||||
|
||||
fun loginUI(username: String, password: String): Flow<LoginData>
|
||||
|
||||
fun refreshUI(refreshToken: String): Flow<String>
|
||||
|
||||
fun loginSimple(username: String, password: String): Flow<String>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ package ca.gosyer.jui.domain.server.service
|
||||
import ca.gosyer.jui.core.prefs.Preference
|
||||
import ca.gosyer.jui.core.prefs.PreferenceStore
|
||||
import ca.gosyer.jui.domain.server.service.host.ServerHostPreference
|
||||
import ca.gosyer.jui.domain.settings.model.AuthMode
|
||||
|
||||
actual class ServerHostPreferences actual constructor(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
@@ -17,46 +18,48 @@ actual class ServerHostPreferences actual constructor(
|
||||
|
||||
// IP
|
||||
private val ip = ServerHostPreference.IP(preferenceStore)
|
||||
|
||||
fun ip(): Preference<String> = ip.preference()
|
||||
|
||||
private val port = ServerHostPreference.Port(preferenceStore)
|
||||
|
||||
fun port(): Preference<Int> = port.preference()
|
||||
|
||||
// Root
|
||||
private val rootPath = ServerHostPreference.RootPath(preferenceStore)
|
||||
|
||||
fun rootPath(): Preference<String> = rootPath.preference()
|
||||
|
||||
// Downloader
|
||||
private val downloadPath = ServerHostPreference.DownloadPath(preferenceStore)
|
||||
|
||||
fun downloadPath(): Preference<String> = downloadPath.preference()
|
||||
|
||||
// Backup
|
||||
private val backupPath = ServerHostPreference.BackupPath(preferenceStore)
|
||||
|
||||
fun backupPath(): Preference<String> = backupPath.preference()
|
||||
|
||||
// LocalSource
|
||||
private val localSourcePath = ServerHostPreference.LocalSourcePath(preferenceStore)
|
||||
|
||||
fun localSourcePath(): Preference<String> = localSourcePath.preference()
|
||||
|
||||
// Authentication
|
||||
private val basicAuthEnabled = ServerHostPreference.BasicAuthEnabled(preferenceStore)
|
||||
|
||||
@Deprecated("")
|
||||
fun basicAuthEnabled(): Preference<Boolean> = basicAuthEnabled.preference()
|
||||
|
||||
private val basicAuthUsername = ServerHostPreference.BasicAuthUsername(preferenceStore)
|
||||
|
||||
@Deprecated("")
|
||||
fun basicAuthUsername(): Preference<String> = basicAuthUsername.preference()
|
||||
|
||||
private val basicAuthPassword = ServerHostPreference.BasicAuthPassword(preferenceStore)
|
||||
|
||||
@Deprecated("")
|
||||
fun basicAuthPassword(): Preference<String> = basicAuthPassword.preference()
|
||||
|
||||
// Authentication
|
||||
private val authMode = ServerHostPreference.AuthMode(preferenceStore)
|
||||
fun authMode(): Preference<AuthMode> = authMode.preference()
|
||||
|
||||
private val authUsername = ServerHostPreference.AuthUsername(preferenceStore)
|
||||
fun authUsername(): Preference<String> = authUsername.preference()
|
||||
|
||||
private val authPassword = ServerHostPreference.AuthPassword(preferenceStore)
|
||||
fun authPassword(): Preference<String> = authPassword.preference()
|
||||
|
||||
fun properties(): Array<String> =
|
||||
listOf(
|
||||
ip,
|
||||
@@ -65,9 +68,9 @@ actual class ServerHostPreferences actual constructor(
|
||||
downloadPath,
|
||||
backupPath,
|
||||
localSourcePath,
|
||||
basicAuthEnabled,
|
||||
basicAuthUsername,
|
||||
basicAuthPassword,
|
||||
authMode,
|
||||
authUsername,
|
||||
authPassword,
|
||||
).mapNotNull {
|
||||
it.getProperty()
|
||||
}.plus(
|
||||
|
||||
@@ -8,6 +8,8 @@ package ca.gosyer.jui.domain.server.service.host
|
||||
|
||||
import ca.gosyer.jui.core.prefs.Preference
|
||||
import ca.gosyer.jui.core.prefs.PreferenceStore
|
||||
import ca.gosyer.jui.core.prefs.getEnum
|
||||
import ca.gosyer.jui.domain.settings.model.AuthMode as ServerAuthMode
|
||||
|
||||
sealed class ServerHostPreference<T : Any> {
|
||||
protected abstract val propertyName: String
|
||||
@@ -63,6 +65,16 @@ sealed class ServerHostPreference<T : Any> {
|
||||
override fun preference(): Preference<Boolean> = preferenceStore.getBoolean(propertyName, defaultValue)
|
||||
}
|
||||
|
||||
sealed class ObjectServerHostPreference<T : Any>(
|
||||
override val preferenceStore: PreferenceStore,
|
||||
override val propertyName: String,
|
||||
override val defaultValue: T,
|
||||
override val serverValue: T = defaultValue,
|
||||
private val getObject: (String, T) -> Preference<T>,
|
||||
) : ServerHostPreference<T>() {
|
||||
override fun preference(): Preference<T> = getObject(propertyName, defaultValue)
|
||||
}
|
||||
|
||||
// Root
|
||||
class RootPath(
|
||||
preferenceStore: PreferenceStore,
|
||||
@@ -116,27 +128,43 @@ sealed class ServerHostPreference<T : Any> {
|
||||
)
|
||||
|
||||
// Authentication
|
||||
@Deprecated("UseAuthMode")
|
||||
class BasicAuthEnabled(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : BooleanServerHostPreference(
|
||||
preferenceStore,
|
||||
"basicAuthEnabled",
|
||||
false,
|
||||
)
|
||||
|
||||
) : BooleanServerHostPreference(preferenceStore, "basicAuthEnabled", false)
|
||||
@Deprecated("UseAuthUsername")
|
||||
class BasicAuthUsername(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : StringServerHostPreference(
|
||||
preferenceStore,
|
||||
"basicAuthUsername",
|
||||
"",
|
||||
)
|
||||
|
||||
) : StringServerHostPreference(preferenceStore, "basicAuthUsername", "")
|
||||
@Deprecated("UseAuthPassword")
|
||||
class BasicAuthPassword(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : StringServerHostPreference(preferenceStore, "basicAuthPassword", "")
|
||||
|
||||
class AuthMode(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : ObjectServerHostPreference<ServerAuthMode>(
|
||||
preferenceStore,
|
||||
"authMode",
|
||||
ServerAuthMode.NONE,
|
||||
getObject = { propertyName, default ->
|
||||
preferenceStore.getEnum(propertyName, default)
|
||||
}
|
||||
)
|
||||
|
||||
class AuthUsername(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : StringServerHostPreference(
|
||||
preferenceStore,
|
||||
"basicAuthPassword",
|
||||
"",
|
||||
)
|
||||
preferenceStore,
|
||||
"authUsername",
|
||||
"",
|
||||
)
|
||||
|
||||
class AuthPassword(
|
||||
preferenceStore: PreferenceStore,
|
||||
) : StringServerHostPreference(
|
||||
preferenceStore,
|
||||
"authPassword",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -319,9 +319,10 @@
|
||||
<string name="host_webui_sub">Whether the server\'s default WebUI is enabled, makes you able to use Suwayomi in your browser</string>
|
||||
<string name="host_open_in_browser">Open Server WebUI on startup</string>
|
||||
<string name="host_open_in_browser_sub">Open the WebUI inside your browser on server startup. Requires the WebUI be enabled</string>
|
||||
<string name="host_basic_auth_sub">Use basic auth to protect your library, requires username and password</string>
|
||||
<string name="host_basic_auth_username">Basic auth username</string>
|
||||
<string name="host_basic_auth_password">Basic auth password</string>
|
||||
<string name="host_auth">Auth mode</string>
|
||||
<string name="host_auth_sub">Use authentication to protect your library, requires username and password</string>
|
||||
<string name="host_auth_username">Auth username</string>
|
||||
<string name="host_auth_password">Auth password</string>
|
||||
<string name="server_url">Server URL</string>
|
||||
<string name="server_port">Server PORT</string>
|
||||
<string name="server_path_prefix">Server Path Prefix</string>
|
||||
@@ -338,6 +339,10 @@
|
||||
<string name="no_auth">No auth</string>
|
||||
<string name="basic_auth">Basic auth</string>
|
||||
<string name="digest_auth">Digest auth</string>
|
||||
<string name="simple_auth">Simple auth</string>
|
||||
<string name="ui_login">UI login</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="auth_username">Auth username</string>
|
||||
<string name="auth_password">Auth password</string>
|
||||
<string name="server_settings_sub">The below settings configure the connected Suwayomi-Server</string>
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
@@ -35,6 +36,8 @@ import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Login
|
||||
import androidx.compose.material.icons.automirrored.rounded.Logout
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
@@ -46,10 +49,15 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.semantics.contentType
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.core.lang.launchIO
|
||||
@@ -59,8 +67,13 @@ import ca.gosyer.jui.domain.server.service.ServerHostPreferences
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.settings.interactor.GetSettings
|
||||
import ca.gosyer.jui.domain.settings.interactor.SetSettings
|
||||
import ca.gosyer.jui.domain.settings.model.AuthMode
|
||||
import ca.gosyer.jui.domain.settings.model.SetSettingsInput
|
||||
import ca.gosyer.jui.domain.settings.model.Settings
|
||||
import ca.gosyer.jui.domain.user.interactor.UserLoginSimple
|
||||
import ca.gosyer.jui.domain.user.interactor.UserLoginUI
|
||||
import ca.gosyer.jui.domain.user.interactor.UserLogout
|
||||
import ca.gosyer.jui.domain.user.service.UserPreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
@@ -95,10 +108,13 @@ import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.datetime.format.char
|
||||
@@ -127,8 +143,12 @@ class SettingsServerScreen : Screen {
|
||||
authChoices = connectionVM.getAuthChoices(),
|
||||
authUsername = connectionVM.authUsername,
|
||||
authPassword = connectionVM.authPassword,
|
||||
authUILoggedIn = connectionVM.authUILoggedIn.collectAsState().value,
|
||||
authSimpleLoggedIn = connectionVM.authSimpleLoggedIn.collectAsState().value,
|
||||
serverSettings = connectionVM.serverSettings.collectAsState().value,
|
||||
hosted = connectionVM.host.collectAsState().value,
|
||||
onLogin = connectionVM::onLogin,
|
||||
onLogout = connectionVM::onLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -194,18 +214,18 @@ class ServerSettings(
|
||||
getInput = { SetSettingsInput(backupTime = it) },
|
||||
)
|
||||
|
||||
// val basicAuthEnabled = getServerFlow(
|
||||
// getSetting = { it.basicAuthEnabled },
|
||||
// getInput = { SetSettingsInput(basicAuthEnabled = it) },
|
||||
// )
|
||||
// val basicAuthPassword = getServerFlow(
|
||||
// getSetting = { it.basicAuthPassword },
|
||||
// getInput = { SetSettingsInput(basicAuthPassword = it) },
|
||||
// )
|
||||
// val basicAuthUsername = getServerFlow(
|
||||
// getSetting = { it.basicAuthUsername },
|
||||
// getInput = { SetSettingsInput(basicAuthUsername = it) },
|
||||
// )
|
||||
val authMode = getServerFlow(
|
||||
getSetting = { it.authMode },
|
||||
getInput = { SetSettingsInput(authMode = it) },
|
||||
)
|
||||
val authPassword = getServerFlow(
|
||||
getSetting = { it.authPassword },
|
||||
getInput = { SetSettingsInput(authPassword = it) },
|
||||
)
|
||||
val authUsername = getServerFlow(
|
||||
getSetting = { it.authUsername },
|
||||
getInput = { SetSettingsInput(authUsername = it) },
|
||||
)
|
||||
val debugLogsEnabled = getServerFlow(
|
||||
getSetting = { it.debugLogsEnabled },
|
||||
getInput = { SetSettingsInput(debugLogsEnabled = it) },
|
||||
@@ -370,6 +390,10 @@ class SettingsServerViewModel(
|
||||
private val setSettings: SetSettings,
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
userPreferences: UserPreferences,
|
||||
private val userLoginSimple: UserLoginSimple,
|
||||
private val userLoginUi: UserLoginUI,
|
||||
private val userLogout: UserLogout,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val serverUrl = serverPreferences.server().asStateIn(scope)
|
||||
@@ -394,6 +418,12 @@ class SettingsServerViewModel(
|
||||
val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope)
|
||||
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
val authUILoggedIn = userPreferences.uiRefreshToken().changes()
|
||||
.map { it.isNotBlank() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, userPreferences.uiRefreshToken().get().isNotBlank())
|
||||
val authSimpleLoggedIn = userPreferences.simpleSession().changes()
|
||||
.map { it.isNotBlank() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, userPreferences.simpleSession().get().isNotBlank())
|
||||
|
||||
@Composable
|
||||
fun getAuthChoices(): ImmutableMap<Auth, String> =
|
||||
@@ -401,11 +431,31 @@ class SettingsServerViewModel(
|
||||
Auth.NONE to stringResource(MR.strings.no_auth),
|
||||
Auth.BASIC to stringResource(MR.strings.basic_auth),
|
||||
Auth.DIGEST to stringResource(MR.strings.digest_auth),
|
||||
Auth.SIMPLE to stringResource(MR.strings.simple_auth),
|
||||
Auth.UI to stringResource(MR.strings.ui_login),
|
||||
)
|
||||
|
||||
val authUsername = serverPreferences.authUsername().asStateIn(scope)
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
fun onLogin(username: String, password: String) {
|
||||
when (auth.value) {
|
||||
Auth.SIMPLE -> {
|
||||
userLoginSimple.asFlow(username, password)
|
||||
.launchIn(scope)
|
||||
}
|
||||
Auth.UI -> {
|
||||
userLoginUi.asFlow(username, password)
|
||||
.launchIn(scope)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun onLogout() {
|
||||
userLogout.asFlow().launchIn(scope)
|
||||
}
|
||||
|
||||
private val _serverSettings = MutableStateFlow<ServerSettings?>(null)
|
||||
val serverSettings = _serverSettings.asStateFlow()
|
||||
|
||||
@@ -443,8 +493,12 @@ fun SettingsServerScreenContent(
|
||||
authChoices: ImmutableMap<Auth, String>,
|
||||
authUsername: PreferenceMutableStateFlow<String>,
|
||||
authPassword: PreferenceMutableStateFlow<String>,
|
||||
authUILoggedIn: Boolean,
|
||||
authSimpleLoggedIn: Boolean,
|
||||
hosted: Boolean,
|
||||
serverSettings: ServerSettings?,
|
||||
onLogin: (String, String) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
@@ -540,18 +594,95 @@ fun SettingsServerScreenContent(
|
||||
item {
|
||||
ChoicePreference(auth, authChoices, stringResource(MR.strings.authentication))
|
||||
}
|
||||
if (authValue != Auth.NONE) {
|
||||
item {
|
||||
EditTextPreference(authUsername, stringResource(MR.strings.auth_username))
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
authPassword,
|
||||
stringResource(MR.strings.auth_password),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
)
|
||||
|
||||
when (authValue) {
|
||||
Auth.NONE -> Unit
|
||||
Auth.BASIC, Auth.DIGEST, Auth.UI, Auth.SIMPLE -> {
|
||||
item {
|
||||
EditTextPreference(authUsername, stringResource(MR.strings.auth_username))
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
authPassword,
|
||||
stringResource(MR.strings.auth_password),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authValue == Auth.UI || authValue == Auth.SIMPLE) {
|
||||
if ((authValue == Auth.SIMPLE && authSimpleLoggedIn) || (authValue == Auth.UI && authUILoggedIn)) {
|
||||
item {
|
||||
PreferenceRow(
|
||||
stringResource(MR.strings.logout),
|
||||
icon = Icons.AutoMirrored.Rounded.Logout,
|
||||
onClick = onLogout,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
val loginDialogState = rememberMaterialDialogState()
|
||||
PreferenceRow(
|
||||
stringResource(MR.strings.login),
|
||||
icon = Icons.AutoMirrored.Rounded.Login,
|
||||
onClick = {
|
||||
loginDialogState.show()
|
||||
},
|
||||
)
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
MaterialDialog(
|
||||
dialogState = loginDialogState,
|
||||
properties = getMaterialDialogProperties(),
|
||||
buttons = {
|
||||
negativeButton(stringResource(MR.strings.action_cancel))
|
||||
positiveButton(
|
||||
stringResource(MR.strings.login),
|
||||
onClick = {
|
||||
onLogin(username, password)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
modifier = Modifier
|
||||
.semantics { contentType = ContentType.Username }
|
||||
.keyboardHandler(
|
||||
singleLine = true,
|
||||
enterAction = {
|
||||
submit()
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
modifier = Modifier
|
||||
.semantics { contentType = ContentType.Password }
|
||||
.keyboardHandler(
|
||||
singleLine = true,
|
||||
enterAction = {
|
||||
submit()
|
||||
},
|
||||
),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
item {
|
||||
Divider()
|
||||
}
|
||||
@@ -814,32 +945,41 @@ fun LazyListScope.ServerSettingsItems(
|
||||
)
|
||||
}
|
||||
|
||||
// item {
|
||||
// SwitchPreference(
|
||||
// preference = serverSettings.basicAuthEnabled,
|
||||
// title = stringResource(MR.strings.basic_auth),
|
||||
// subtitle = stringResource(MR.strings.host_basic_auth_sub),
|
||||
// enabled = !hosted,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// item {
|
||||
// val basicAuthEnabledValue by serverSettings.basicAuthEnabled.collectAsState()
|
||||
// EditTextPreference(
|
||||
// preference = serverSettings.basicAuthUsername,
|
||||
// title = stringResource(MR.strings.host_basic_auth_username),
|
||||
// enabled = basicAuthEnabledValue && !hosted,
|
||||
// )
|
||||
// }
|
||||
// item {
|
||||
// val basicAuthEnabledValue by serverSettings.basicAuthEnabled.collectAsState()
|
||||
// EditTextPreference(
|
||||
// preference = serverSettings.basicAuthPassword,
|
||||
// title = stringResource(MR.strings.host_basic_auth_password),
|
||||
// visualTransformation = PasswordVisualTransformation(),
|
||||
// enabled = basicAuthEnabledValue && !hosted,
|
||||
// )
|
||||
// }
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = serverSettings.authMode,
|
||||
title = stringResource(MR.strings.host_auth),
|
||||
subtitle = stringResource(MR.strings.host_auth_sub),
|
||||
choices = (AuthMode.entries - AuthMode.UNKNOWN__).associateWith {
|
||||
when (it) {
|
||||
AuthMode.NONE -> stringResource(MR.strings.no_auth)
|
||||
AuthMode.BASIC_AUTH -> stringResource(MR.strings.basic_auth)
|
||||
AuthMode.SIMPLE_LOGIN -> stringResource(MR.strings.simple_auth)
|
||||
AuthMode.UI_LOGIN -> stringResource(MR.strings.ui_login)
|
||||
AuthMode.UNKNOWN__ -> ""
|
||||
}
|
||||
}.toImmutableMap(),
|
||||
enabled = !hosted,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
val authModeValue by serverSettings.authMode.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = serverSettings.authUsername,
|
||||
title = stringResource(MR.strings.host_auth_username),
|
||||
enabled = authModeValue != AuthMode.NONE && !hosted,
|
||||
)
|
||||
}
|
||||
item {
|
||||
val authModeValue by serverSettings.authMode.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = serverSettings.authPassword,
|
||||
title = stringResource(MR.strings.host_auth_password),
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
enabled = authModeValue != AuthMode.NONE && !hosted,
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = serverSettings.flareSolverrEnabled,
|
||||
|
||||
@@ -19,7 +19,9 @@ import ca.gosyer.jui.domain.server.model.Auth
|
||||
import ca.gosyer.jui.domain.server.service.ServerHostPreferences
|
||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||
import ca.gosyer.jui.domain.server.service.ServerService
|
||||
import ca.gosyer.jui.domain.settings.model.AuthMode
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.prefs.ChoicePreference
|
||||
import ca.gosyer.jui.ui.base.prefs.EditTextPreference
|
||||
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.jui.ui.base.prefs.SwitchPreference
|
||||
@@ -29,6 +31,7 @@ import ca.gosyer.jui.uicore.prefs.asStringStateIn
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -39,7 +42,7 @@ import me.tatarka.inject.annotations.Inject
|
||||
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit {
|
||||
val serverVm = viewModel()
|
||||
val hostValue by serverVm.host.collectAsState()
|
||||
val basicAuthEnabledValue by serverVm.basicAuthEnabled.collectAsState()
|
||||
val authMode by serverVm.hostAuthMode.collectAsState()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
@@ -50,7 +53,7 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie
|
||||
return {
|
||||
ServerHostItems(
|
||||
hostValue = hostValue,
|
||||
basicAuthEnabledValue = basicAuthEnabledValue,
|
||||
authEnabledValue = authMode != AuthMode.NONE,
|
||||
serverSettingChanged = serverVm::serverSettingChanged,
|
||||
host = serverVm.host,
|
||||
ip = serverVm.ip,
|
||||
@@ -59,9 +62,9 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie
|
||||
downloadPath = serverVm.downloadPath,
|
||||
backupPath = serverVm.backupPath,
|
||||
localSourcePath = serverVm.localSourcePath,
|
||||
basicAuthEnabled = serverVm.basicAuthEnabled,
|
||||
basicAuthUsername = serverVm.basicAuthUsername,
|
||||
basicAuthPassword = serverVm.basicAuthPassword,
|
||||
authMode = serverVm.hostAuthMode,
|
||||
authUsername = serverVm.hostAuthUsername,
|
||||
authPassword = serverVm.hostAuthPassword,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -92,9 +95,9 @@ actual class SettingsServerHostViewModel(
|
||||
val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope)
|
||||
|
||||
// Authentication
|
||||
val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope)
|
||||
val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope)
|
||||
val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope)
|
||||
val hostAuthMode = serverHostPreferences.authMode().asStateIn(scope)
|
||||
val hostAuthUsername = serverHostPreferences.authUsername().asStateIn(scope)
|
||||
val hostAuthPassword = serverHostPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
private val _serverSettingChanged = MutableStateFlow(false)
|
||||
val serverSettingChanged = _serverSettingChanged.asStateFlow()
|
||||
@@ -115,16 +118,18 @@ actual class SettingsServerHostViewModel(
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
init {
|
||||
combine(host, basicAuthEnabled, basicAuthUsername, basicAuthPassword) { host, enabled, username, password ->
|
||||
combine(host, hostAuthMode, hostAuthUsername, hostAuthPassword) { host, mode, username, password ->
|
||||
if (host) {
|
||||
if (enabled) {
|
||||
auth.value = Auth.BASIC
|
||||
authUsername.value = username
|
||||
authPassword.value = password
|
||||
} else {
|
||||
auth.value = Auth.NONE
|
||||
authUsername.value = ""
|
||||
authPassword.value = ""
|
||||
when (mode) {
|
||||
AuthMode.NONE -> auth.value = Auth.NONE
|
||||
AuthMode.BASIC_AUTH -> {
|
||||
auth.value = Auth.BASIC
|
||||
authUsername.value = username
|
||||
authPassword.value = password
|
||||
}
|
||||
AuthMode.SIMPLE_LOGIN -> auth.value = Auth.SIMPLE
|
||||
AuthMode.UI_LOGIN -> auth.value = Auth.UI
|
||||
AuthMode.UNKNOWN__ -> Unit
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
@@ -133,7 +138,7 @@ actual class SettingsServerHostViewModel(
|
||||
|
||||
fun LazyListScope.ServerHostItems(
|
||||
hostValue: Boolean,
|
||||
basicAuthEnabledValue: Boolean,
|
||||
authEnabledValue: Boolean,
|
||||
serverSettingChanged: () -> Unit,
|
||||
host: MutableStateFlow<Boolean>,
|
||||
ip: MutableStateFlow<String>,
|
||||
@@ -142,9 +147,9 @@ fun LazyListScope.ServerHostItems(
|
||||
downloadPath: MutableStateFlow<String>,
|
||||
backupPath: MutableStateFlow<String>,
|
||||
localSourcePath: MutableStateFlow<String>,
|
||||
basicAuthEnabled: MutableStateFlow<Boolean>,
|
||||
basicAuthUsername: MutableStateFlow<String>,
|
||||
basicAuthPassword: MutableStateFlow<String>,
|
||||
authMode: MutableStateFlow<AuthMode>,
|
||||
authUsername: MutableStateFlow<String>,
|
||||
authPassword: MutableStateFlow<String>,
|
||||
) {
|
||||
item {
|
||||
SwitchPreference(preference = host, title = stringResource(MR.strings.host_server))
|
||||
@@ -260,28 +265,37 @@ fun LazyListScope.ServerHostItems(
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = basicAuthEnabled,
|
||||
title = stringResource(MR.strings.basic_auth),
|
||||
subtitle = stringResource(MR.strings.host_basic_auth_sub),
|
||||
ChoicePreference(
|
||||
preference = authMode,
|
||||
title = stringResource(MR.strings.host_auth),
|
||||
subtitle = stringResource(MR.strings.host_auth_sub),
|
||||
choices = (AuthMode.entries - AuthMode.UNKNOWN__).associateWith {
|
||||
when (it) {
|
||||
AuthMode.NONE -> stringResource(MR.strings.no_auth)
|
||||
AuthMode.BASIC_AUTH -> stringResource(MR.strings.basic_auth)
|
||||
AuthMode.SIMPLE_LOGIN -> stringResource(MR.strings.simple_auth)
|
||||
AuthMode.UI_LOGIN -> stringResource(MR.strings.ui_login)
|
||||
AuthMode.UNKNOWN__ -> ""
|
||||
}
|
||||
}.toImmutableMap(),
|
||||
changeListener = serverSettingChanged,
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
preference = basicAuthUsername,
|
||||
title = stringResource(MR.strings.host_basic_auth_username),
|
||||
preference = authUsername,
|
||||
title = stringResource(MR.strings.host_auth_username),
|
||||
changeListener = serverSettingChanged,
|
||||
enabled = basicAuthEnabledValue,
|
||||
enabled = authEnabledValue,
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
preference = basicAuthPassword,
|
||||
title = stringResource(MR.strings.host_basic_auth_password),
|
||||
preference = authPassword,
|
||||
title = stringResource(MR.strings.host_auth_password),
|
||||
changeListener = serverSettingChanged,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
enabled = basicAuthEnabledValue,
|
||||
enabled = authEnabledValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user