[#1349] Basic Cookie Authentication (#1498)

* [#1349] Stub basic cookie authentication

* [#1349] Basic login page

Also adjusts WebView header color and shadow to match WebUI. WebUI uses
a background-image gradient to change the perceived color, which was not
noticed originally.

* [#1349] Handle login post

* [#1349] Redirect to previous URL

* [#1349] Return a basic 401 for api endpoints

Instead of redirecting to a visual login page, API should just indicate
the bad state

* Use more appropriate 303 redirect

* Update server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Update server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Lint

* Transition to AuthMode enum with migration path

* Make basicAuthEnabled auto property, Lint

* ConfigManager: Make sure to re-parse the config after migration

* basicAuth{Username,Password} -> auth{Username,Password}

* Lint

* Update server settings backup model

* Update comment

* Minor cleanup

* Improve backup legacy settings fix

* Lint

* Simplify config value migration

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-07-06 18:08:29 +02:00
committed by GitHub
parent 1411c02e18
commit 68a131dbeb
15 changed files with 432 additions and 34 deletions

View File

@@ -138,7 +138,7 @@ open class ConfigManager {
* - adds missing settings * - adds missing settings
* - removes outdated settings * - removes outdated settings
*/ */
fun updateUserConfig() { fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) {
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val userConfig = getUserConfig() val userConfig = getUserConfig()
@@ -162,7 +162,11 @@ open class ConfigManager {
) )
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) } }.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
newUserConfigDoc =
migrate(newUserConfigDoc, internalConfig)
userConfigFile.writeText(newUserConfigDoc.render()) userConfigFile.writeText(newUserConfigDoc.render())
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
} }
} }

View File

@@ -43,7 +43,8 @@ object WebView : Websocket<String>() {
} }
} }
@Serializable public sealed class TypeObject @Serializable
sealed class TypeObject
@Serializable @Serializable
@SerialName("loadUrl") @SerialName("loadUrl")
@@ -62,7 +63,7 @@ object WebView : Websocket<String>() {
@Serializable @Serializable
@SerialName("event") @SerialName("event")
public data class JsEventMessage( data class JsEventMessage(
val eventType: String, val eventType: String,
val clickX: Float, val clickX: Float,
val clickY: Float, val clickY: Float,
@@ -94,7 +95,6 @@ object WebView : Websocket<String>() {
logger.info { "Resize browser" } logger.info { "Resize browser" }
} }
is JsEventMessage -> { is JsEventMessage -> {
val type = event.eventType
dr.event(event) dr.event(event)
} }
} }

View File

@@ -155,6 +155,9 @@ class SettingsMutation {
updateSetting(settings.updateMangas, serverConfig.updateMangas) updateSetting(settings.updateMangas, serverConfig.updateMangas)
// Authentication // Authentication
updateSetting(settings.authMode, serverConfig.authMode)
updateSetting(settings.authUsername, serverConfig.authUsername)
updateSetting(settings.authPassword, serverConfig.authPassword)
updateSetting(settings.basicAuthEnabled, serverConfig.basicAuthEnabled) updateSetting(settings.basicAuthEnabled, serverConfig.basicAuthEnabled)
updateSetting(settings.basicAuthUsername, serverConfig.basicAuthUsername) updateSetting(settings.basicAuthUsername, serverConfig.basicAuthUsername)
updateSetting(settings.basicAuthPassword, serverConfig.basicAuthPassword) updateSetting(settings.basicAuthPassword, serverConfig.basicAuthPassword)

View File

@@ -0,0 +1,13 @@
package suwayomi.tachidesk.graphql.types
enum class AuthMode {
NONE,
BASIC_AUTH,
SIMPLE_LOGIN,
// TODO: ACCOUNT for #623
;
companion object {
fun from(channel: String): AuthMode = entries.find { it.name.lowercase() == channel.lowercase() } ?: NONE
}
}

View File

@@ -63,8 +63,17 @@ interface Settings : Node {
val updateMangas: Boolean? val updateMangas: Boolean?
// Authentication // Authentication
val authMode: AuthMode?
val authUsername: String?
val authPassword: String?
@GraphQLDeprecated("Removed - prefer authMode")
val basicAuthEnabled: Boolean? val basicAuthEnabled: Boolean?
@GraphQLDeprecated("Removed - prefer authUsername")
val basicAuthUsername: String? val basicAuthUsername: String?
@GraphQLDeprecated("Removed - prefer authPassword")
val basicAuthPassword: String? val basicAuthPassword: String?
// misc // misc
@@ -144,8 +153,14 @@ data class PartialSettingsType(
override val globalUpdateInterval: Double?, override val globalUpdateInterval: Double?,
override val updateMangas: Boolean?, override val updateMangas: Boolean?,
// Authentication // Authentication
override val authMode: AuthMode?,
override val authUsername: String?,
override val authPassword: String?,
@GraphQLDeprecated("Removed - prefer authMode")
override val basicAuthEnabled: Boolean?, override val basicAuthEnabled: Boolean?,
@GraphQLDeprecated("Removed - prefer authUsername")
override val basicAuthUsername: String?, override val basicAuthUsername: String?,
@GraphQLDeprecated("Removed - prefer authPassword")
override val basicAuthPassword: String?, override val basicAuthPassword: String?,
// misc // misc
override val debugLogsEnabled: Boolean?, override val debugLogsEnabled: Boolean?,
@@ -219,8 +234,14 @@ class SettingsType(
override val globalUpdateInterval: Double, override val globalUpdateInterval: Double,
override val updateMangas: Boolean, override val updateMangas: Boolean,
// Authentication // Authentication
override val authMode: AuthMode,
override val authUsername: String,
override val authPassword: String,
@GraphQLDeprecated("Removed - prefer authMode")
override val basicAuthEnabled: Boolean, override val basicAuthEnabled: Boolean,
@GraphQLDeprecated("Removed - prefer authUsername")
override val basicAuthUsername: String, override val basicAuthUsername: String,
@GraphQLDeprecated("Removed - prefer authPassword")
override val basicAuthPassword: String, override val basicAuthPassword: String,
// misc // misc
override val debugLogsEnabled: Boolean, override val debugLogsEnabled: Boolean,
@@ -289,6 +310,9 @@ class SettingsType(
config.globalUpdateInterval.value, config.globalUpdateInterval.value,
config.updateMangas.value, config.updateMangas.value,
// Authentication // Authentication
config.authMode.value,
config.authUsername.value,
config.authPassword.value,
config.basicAuthEnabled.value, config.basicAuthEnabled.value,
config.basicAuthUsername.value, config.basicAuthUsername.value,
config.basicAuthPassword.value, config.basicAuthPassword.value,

View File

@@ -419,9 +419,12 @@ object ProtoBackupExport : ProtoBackupBase() {
globalUpdateInterval = serverConfig.globalUpdateInterval.value, globalUpdateInterval = serverConfig.globalUpdateInterval.value,
updateMangas = serverConfig.updateMangas.value, updateMangas = serverConfig.updateMangas.value,
// Authentication // Authentication
basicAuthEnabled = serverConfig.basicAuthEnabled.value, authMode = serverConfig.authMode.value,
basicAuthUsername = serverConfig.basicAuthUsername.value, authUsername = serverConfig.authUsername.value,
basicAuthPassword = serverConfig.basicAuthPassword.value, authPassword = serverConfig.authPassword.value,
basicAuthEnabled = false,
basicAuthUsername = null,
basicAuthPassword = null,
// misc // misc
debugLogsEnabled = serverConfig.debugLogsEnabled.value, debugLogsEnabled = serverConfig.debugLogsEnabled.value,
gqlDebugLogsEnabled = false, // deprecated gqlDebugLogsEnabled = false, // deprecated

View File

@@ -32,6 +32,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.graphql.mutations.SettingsMutation import suwayomi.tachidesk.graphql.mutations.SettingsMutation
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
@@ -57,6 +58,7 @@ import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.database.dbTransaction import suwayomi.tachidesk.server.database.dbTransaction
import suwayomi.tachidesk.server.serverConfig
import java.io.InputStream import java.io.InputStream
import java.util.Date import java.util.Date
import java.util.Timer import java.util.Timer
@@ -525,7 +527,15 @@ object ProtoBackupImport : ProtoBackupBase() {
return return
} }
SettingsMutation().updateSettings(backupServerSettings) SettingsMutation().updateSettings(
backupServerSettings.copy(
// legacy settings cannot overwrite new settings
basicAuthEnabled =
backupServerSettings.basicAuthEnabled.takeIf {
serverConfig.authMode.value == AuthMode.NONE
},
),
)
} }
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0) private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)

View File

@@ -3,6 +3,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.Settings
import suwayomi.tachidesk.graphql.types.WebUIChannel import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIFlavor
@@ -45,9 +46,13 @@ data class BackupServerSettings(
@ProtoNumber(27) override var globalUpdateInterval: Double, @ProtoNumber(27) override var globalUpdateInterval: Double,
@ProtoNumber(28) override var updateMangas: Boolean, @ProtoNumber(28) override var updateMangas: Boolean,
// Authentication // Authentication
@ProtoNumber(29) override var basicAuthEnabled: Boolean, @ProtoNumber(56) override var authMode: AuthMode,
@ProtoNumber(30) override var basicAuthUsername: String, @ProtoNumber(29) override var basicAuthEnabled: Boolean?,
@ProtoNumber(31) override var basicAuthPassword: String, @ProtoNumber(30) override var authUsername: String,
@ProtoNumber(31) override var authPassword: String,
// deprecated
@ProtoNumber(99991) override var basicAuthUsername: String?,
@ProtoNumber(99992) override var basicAuthPassword: String?,
// misc // misc
@ProtoNumber(32) override var debugLogsEnabled: Boolean, @ProtoNumber(32) override var debugLogsEnabled: Boolean,
@ProtoNumber(33) override var gqlDebugLogsEnabled: Boolean, @ProtoNumber(33) override var gqlDebugLogsEnabled: Boolean,

View File

@@ -11,6 +11,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.http.HandlerType import io.javalin.http.HandlerType
import io.javalin.http.HttpStatus
import io.javalin.http.RedirectResponse
import io.javalin.http.UnauthorizedResponse import io.javalin.http.UnauthorizedResponse
import io.javalin.http.staticfiles.Location import io.javalin.http.staticfiles.Location
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -22,14 +24,19 @@ import kotlinx.coroutines.runBlocking
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.graphql.GraphQL
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.opds.OpdsAPI import suwayomi.tachidesk.opds.OpdsAPI
import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.time.Duration.Companion.days
object JavalinSetup { object JavalinSetup {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -111,8 +118,49 @@ object JavalinSetup {
} }
} }
app.get("/login.html") { ctx ->
var page =
this::class.java
.getResourceAsStream("/static/login.html")!!
.use { it.readAllBytes() }
.toString(Charsets.UTF_8)
page = page.replace("[VERSION]", BuildConfig.VERSION).replace("[ERROR]", "")
ctx.header("content-type", "text/html")
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
ctx.result(page)
}
app.post("/login.html") { ctx ->
val username = ctx.formParam("user")
val password = ctx.formParam("pass")
val isValid =
username == serverConfig.authUsername.value &&
password == serverConfig.authPassword.value
if (isValid) {
val redirect = ctx.queryParam("redirect") ?: "/"
// NOTE: We currently have no session handler attached.
// Thus, all sessions are stored in memory and not persisted.
// Furthermore, default session timeout appears to be 30m
ctx.header("Location", redirect)
ctx.sessionAttribute("logged-in", username)
throw RedirectResponse(HttpStatus.SEE_OTHER)
}
var page =
this::class.java
.getResourceAsStream("/static/login.html")!!
.use { it.readAllBytes() }
.toString(Charsets.UTF_8)
page = page.replace("[VERSION]", BuildConfig.VERSION).replace("[ERROR]", "Invalid username or password")
ctx.header("content-type", "text/html")
ctx.req().session.invalidate()
ctx.result(page)
}
app.beforeMatched { ctx -> app.beforeMatched { ctx ->
val isWebManifest = listOf("site.webmanifest", "manifest.json").any { ctx.path().endsWith(it) } val isWebManifest = listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
val isPreFlight = ctx.method() == HandlerType.OPTIONS val isPreFlight = ctx.method() == HandlerType.OPTIONS
val requiresAuthentication = !isPreFlight && !isWebManifest val requiresAuthentication = !isPreFlight && !isWebManifest
@@ -120,14 +168,31 @@ object JavalinSetup {
return@beforeMatched return@beforeMatched
} }
val authMode = serverConfig.authMode.value ?: AuthMode.NONE
fun credentialsValid(): Boolean { fun credentialsValid(): Boolean {
val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false
val (username, password) = basicAuthCredentials val (username, password) = basicAuthCredentials
return username == serverConfig.basicAuthUsername.value && return username == serverConfig.authUsername.value &&
password == serverConfig.basicAuthPassword.value password == serverConfig.authPassword.value
} }
if (serverConfig.basicAuthEnabled.value && !credentialsValid()) { fun cookieValid(): Boolean {
val username = ctx.sessionAttribute<String>("logged-in") ?: return false
return username == serverConfig.authUsername.value
}
if (authMode == AuthMode.SIMPLE_LOGIN && !cookieValid() && ctx.path().startsWith("/api")) {
throw UnauthorizedResponse()
}
if (authMode == AuthMode.SIMPLE_LOGIN && !cookieValid()) {
val url = "/login.html?redirect=" + URLEncoder.encode(ctx.fullUrl(), StandardCharsets.UTF_8)
ctx.header("Location", url)
throw RedirectResponse(HttpStatus.SEE_OTHER)
}
if (authMode == AuthMode.BASIC_AUTH && !credentialsValid()) {
ctx.header("WWW-Authenticate", "Basic") ctx.header("WWW-Authenticate", "Basic")
throw UnauthorizedResponse() throw UnauthorizedResponse()
} }

View File

@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.graphql.types.WebUIChannel import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIInterface import suwayomi.tachidesk.graphql.types.WebUIInterface
@@ -89,6 +90,42 @@ class ServerConfig(
.map { configAdapter.toType(it) } .map { configAdapter.toType(it) }
} }
open inner class MigratedConfigValue<T>(
private val readMigrated: () -> Any,
private val setMigrated: (T) -> Unit,
) {
private var flow: MutableStateFlow<T>? = null
open fun getValueFromConfig(
thisRef: ServerConfig,
property: KProperty<*>,
): Any = readMigrated()
operator fun getValue(
thisRef: ServerConfig,
property: KProperty<*>,
): MutableStateFlow<T> {
if (flow != null) {
return flow!!
}
@Suppress("UNCHECKED_CAST")
val value = getValueFromConfig(thisRef, property) as T
val stateFlow = MutableStateFlow(value)
flow = stateFlow
stateFlow
.drop(1)
.distinctUntilChanged()
.filter { it != getValueFromConfig(thisRef, property) }
.onEach(setMigrated)
.launchIn(mutableConfigValueScope)
return stateFlow
}
}
val ip: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val ip: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter) val port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
@@ -120,14 +157,6 @@ class ServerConfig(
// extensions // extensions
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter) val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter)
// playwright webview
val playwrightBrowser: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val playwrightWsEndpoint: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val playwrightSandbox: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
// webview
val webviewImpl: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
// requests // requests
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter) val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
@@ -139,9 +168,20 @@ class ServerConfig(
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
// Authentication // Authentication
val basicAuthEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue(EnumConfigAdapter(AuthMode::class.java))
val basicAuthUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val authUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val basicAuthPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter) val authPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
authMode.value == AuthMode.BASIC_AUTH
}) {
authMode.value = if (it) AuthMode.BASIC_AUTH else AuthMode.NONE
}
val basicAuthUsername: MutableStateFlow<String> by MigratedConfigValue({ authUsername.value }) {
authUsername.value = it
}
val basicAuthPassword: MutableStateFlow<String> by MigratedConfigValue({ authPassword.value }) {
authPassword.value = it
}
// misc // misc
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter) val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)

View File

@@ -9,7 +9,12 @@ package suwayomi.tachidesk.server
import android.os.Looper import android.os.Looper
import ch.qos.logback.classic.Level import ch.qos.logback.classic.Level
import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.parser.ConfigDocument
import dev.datlag.kcef.KCEF import dev.datlag.kcef.KCEF
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.createAppModule import eu.kanade.tachiyomi.createAppModule
@@ -32,12 +37,14 @@ import org.koin.core.context.startKoin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.module import org.koin.dsl.module
import suwayomi.tachidesk.global.impl.KcefWebView.Companion.toCefCookie import suwayomi.tachidesk.global.impl.KcefWebView.Companion.toCefCookie
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.i18n.LocalizationHelper import suwayomi.tachidesk.i18n.LocalizationHelper
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.Updater import suwayomi.tachidesk.manga.impl.update.Updater
import suwayomi.tachidesk.manga.impl.util.lang.renameTo import suwayomi.tachidesk.manga.impl.util.lang.renameTo
import suwayomi.tachidesk.server.BooleanConfigAdapter
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
@@ -127,6 +134,29 @@ fun setupLogLevelUpdating(
) )
} }
fun <T : Any> migrateConfig(
configDocument: ConfigDocument,
config: Config,
configKey: String,
toConfigKey: String,
toType: (ConfigValue) -> T?,
): ConfigDocument {
try {
val configValue = config.getValue(configKey)
val typedValue = toType(configValue)
if (typedValue != null) {
return configDocument.withValue(
toConfigKey,
ConfigValueFactory.fromAnyRef(typedValue),
)
}
} catch (_: ConfigException) {
// ignore, likely already migrated
}
return configDocument
}
fun serverModule(applicationDirs: ApplicationDirs): Module = fun serverModule(applicationDirs: ApplicationDirs): Module =
module { module {
single { applicationDirs } single { applicationDirs }
@@ -268,7 +298,40 @@ fun applicationSetup() {
} }
} else { } else {
// make sure the user config file is up-to-date // make sure the user config file is up-to-date
GlobalConfigManager.updateUserConfig() GlobalConfigManager.updateUserConfig { config ->
var updatedConfig = this
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthEnabled",
"server.authMode",
toType = {
if (it.unwrapped() as? Boolean == true) {
AuthMode.BASIC_AUTH.name
} else {
null
}
},
)
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthUsername",
"server.authUsername",
toType = { it.unwrapped() as? String },
)
updatedConfig =
migrateConfig(
updatedConfig,
config,
"server.basicAuthPassword",
"server.authPassword",
toType = { it.unwrapped() as? String },
)
updatedConfig
}
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Exception while creating initial server.conf" } logger.error(e) { "Exception while creating initial server.conf" }

View File

@@ -43,9 +43,9 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
# Authentication # Authentication
server.basicAuthEnabled = false server.authMode = "none" # none, basic_auth or simple_login
server.basicAuthUsername = "" server.authUsername = ""
server.basicAuthPassword = "" server.authPassword = ""
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false

View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Suwayomi Login</title>
<style>
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
display: flex;
flex-direction: column;
background-color: rgb(12, 16, 33);
font-family: "Roboto","Helvetica","Arial",sans-serif;
font-weight: 400;
letter-spacing: 0em;
}
button[disabled], input[disabled] {
cursor: not-allowed;
}
header {
background-color: rgb(34, 38, 53);
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px -1px, rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px;
color: #fff;
padding: 8px 32px;
}
header h1, header p {
margin: 0;
}
footer {
color: #fff;
padding: 8px;
}
footer p {
margin: 0;
font-size: 0.7rem;
}
main {
height: 100%;
}
main {
position: relative;
padding-top: 24px;
}
form {
margin: 8px;
padding: 8px 24px;
border-radius: 8px;
border: 1px solid rgb(12, 16, 33);
background-color: rgb(6, 8, 16);
color: white;
}
.error {
margin: 8px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #b71c1c;
background-color: #c62828;
color: white;
}
.error:empty {
display: none;
}
form label {
cursor: pointer;
}
form button {
all: unset;
padding: 8px;
line-height: 1.75;
text-align: center;
min-width: 64px;
border-radius: 4px;
padding: 6px 8px;
color: rgb(91, 116, 239);
text-transform: uppercase;
letter-spacing: 0.02857em;
}
form button:not([disabled]) {
cursor: pointer;
}
form button:not([disabled]):hover {
background-color: rgba(91, 116, 239, 0.08);
}
form input {
all: unset;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.23);
padding: 6px 12px;
width: auto;
min-width: 0;
}
form input:hover {
border-color: white;
}
form input:focus {
border-color: rgb(91, 116, 239);
}
form .controls {
display: grid;
align-items: center;
grid-template-columns: 1fr;
}
form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 6px;
}
form .submit {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 24px;
}
@media (min-width: 500px) {
form {
width: 100%;
max-width: 450px;
margin: 8px auto;
}
.error {
width: 100%;
max-width: 450px;
margin: 8px auto;
}
form .controls {
grid-template-columns: auto 1fr;
column-gap: 16px;
row-gap: 6px;
}
form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 0px;
}
}
</style>
</head>
<body>
<header>
<h1>Suwayomi</h1>
</header>
<main>
<div class="error">[ERROR]</div>
<form method="POST">
<h2>Login</h2>
<div class="controls">
<label for="user">Username:</label>
<input type="text" name="user" id="user" required placeholder="Type username..."/>
<label for="pass">Password:</label>
<input type="password" name="pass" id="pass" required placeholder="Secret..."/>
</div>
<div class="submit">
<button type="submit">Log In</button>
</div>
</form>
</main>
<footer>
<p>Suwayomi: Version [VERSION]</p>
</footer>
</body>
</html>

View File

@@ -15,6 +15,9 @@
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: "Roboto","Helvetica","Arial",sans-serif;
font-weight: 400;
letter-spacing: 0em;
} }
body.disconnected::after { body.disconnected::after {
content: 'Disconnected, please refresh'; content: 'Disconnected, please refresh';
@@ -30,7 +33,8 @@
cursor: not-allowed; cursor: not-allowed;
} }
header { header {
background-color: rgb(12, 16, 33); background-color: rgb(34, 38, 53);
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px -1px, rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px;
color: #fff; color: #fff;
padding: 8px 32px; padding: 8px 32px;
} }

View File

@@ -43,9 +43,9 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
# Authentication # Authentication
server.basicAuthEnabled = false server.authMode = "none" # none, basic_auth or simple_login
server.basicAuthUsername = "" server.authUsername = ""
server.basicAuthPassword = "" server.authPassword = ""
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false