Initial auth support(ws currently broken, bad login dialog)

This commit is contained in:
Syer10
2025-10-15 16:23:11 -04:00
parent 0245ef25fe
commit f0a9f7aff1
21 changed files with 790 additions and 139 deletions

View File

@@ -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"

View 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
}
}

View File

@@ -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,

View File

@@ -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(";"))
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,
)
)

View File

@@ -15,4 +15,6 @@ enum class Auth {
NONE,
BASIC,
DIGEST,
SIMPLE,
UI,
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,6 @@
package ca.gosyer.jui.domain.user.model
data class LoginData(
val refreshToken: String,
val accessToken: String,
)

View File

@@ -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", "")
}

View File

@@ -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>
}

View File

@@ -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(

View File

@@ -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",
"",
)
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,
)
}
}