mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
* [#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:
@@ -138,7 +138,7 @@ open class ConfigManager {
|
||||
* - adds missing settings
|
||||
* - removes outdated settings
|
||||
*/
|
||||
fun updateUserConfig() {
|
||||
fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) {
|
||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||
val userConfig = getUserConfig()
|
||||
|
||||
@@ -162,7 +162,11 @@ open class ConfigManager {
|
||||
)
|
||||
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
||||
|
||||
newUserConfigDoc =
|
||||
migrate(newUserConfigDoc, internalConfig)
|
||||
|
||||
userConfigFile.writeText(newUserConfigDoc.render())
|
||||
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ object WebView : Websocket<String>() {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable public sealed class TypeObject
|
||||
@Serializable
|
||||
sealed class TypeObject
|
||||
|
||||
@Serializable
|
||||
@SerialName("loadUrl")
|
||||
@@ -62,7 +63,7 @@ object WebView : Websocket<String>() {
|
||||
|
||||
@Serializable
|
||||
@SerialName("event")
|
||||
public data class JsEventMessage(
|
||||
data class JsEventMessage(
|
||||
val eventType: String,
|
||||
val clickX: Float,
|
||||
val clickY: Float,
|
||||
@@ -94,7 +95,6 @@ object WebView : Websocket<String>() {
|
||||
logger.info { "Resize browser" }
|
||||
}
|
||||
is JsEventMessage -> {
|
||||
val type = event.eventType
|
||||
dr.event(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,9 @@ class SettingsMutation {
|
||||
updateSetting(settings.updateMangas, serverConfig.updateMangas)
|
||||
|
||||
// Authentication
|
||||
updateSetting(settings.authMode, serverConfig.authMode)
|
||||
updateSetting(settings.authUsername, serverConfig.authUsername)
|
||||
updateSetting(settings.authPassword, serverConfig.authPassword)
|
||||
updateSetting(settings.basicAuthEnabled, serverConfig.basicAuthEnabled)
|
||||
updateSetting(settings.basicAuthUsername, serverConfig.basicAuthUsername)
|
||||
updateSetting(settings.basicAuthPassword, serverConfig.basicAuthPassword)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,17 @@ interface Settings : Node {
|
||||
val updateMangas: Boolean?
|
||||
|
||||
// Authentication
|
||||
val authMode: AuthMode?
|
||||
val authUsername: String?
|
||||
val authPassword: String?
|
||||
|
||||
@GraphQLDeprecated("Removed - prefer authMode")
|
||||
val basicAuthEnabled: Boolean?
|
||||
|
||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
||||
val basicAuthUsername: String?
|
||||
|
||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
||||
val basicAuthPassword: String?
|
||||
|
||||
// misc
|
||||
@@ -144,8 +153,14 @@ data class PartialSettingsType(
|
||||
override val globalUpdateInterval: Double?,
|
||||
override val updateMangas: Boolean?,
|
||||
// Authentication
|
||||
override val authMode: AuthMode?,
|
||||
override val authUsername: String?,
|
||||
override val authPassword: String?,
|
||||
@GraphQLDeprecated("Removed - prefer authMode")
|
||||
override val basicAuthEnabled: Boolean?,
|
||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
||||
override val basicAuthUsername: String?,
|
||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
||||
override val basicAuthPassword: String?,
|
||||
// misc
|
||||
override val debugLogsEnabled: Boolean?,
|
||||
@@ -219,8 +234,14 @@ class SettingsType(
|
||||
override val globalUpdateInterval: Double,
|
||||
override val updateMangas: Boolean,
|
||||
// Authentication
|
||||
override val authMode: AuthMode,
|
||||
override val authUsername: String,
|
||||
override val authPassword: String,
|
||||
@GraphQLDeprecated("Removed - prefer authMode")
|
||||
override val basicAuthEnabled: Boolean,
|
||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
||||
override val basicAuthUsername: String,
|
||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
||||
override val basicAuthPassword: String,
|
||||
// misc
|
||||
override val debugLogsEnabled: Boolean,
|
||||
@@ -289,6 +310,9 @@ class SettingsType(
|
||||
config.globalUpdateInterval.value,
|
||||
config.updateMangas.value,
|
||||
// Authentication
|
||||
config.authMode.value,
|
||||
config.authUsername.value,
|
||||
config.authPassword.value,
|
||||
config.basicAuthEnabled.value,
|
||||
config.basicAuthUsername.value,
|
||||
config.basicAuthPassword.value,
|
||||
|
||||
@@ -419,9 +419,12 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
globalUpdateInterval = serverConfig.globalUpdateInterval.value,
|
||||
updateMangas = serverConfig.updateMangas.value,
|
||||
// Authentication
|
||||
basicAuthEnabled = serverConfig.basicAuthEnabled.value,
|
||||
basicAuthUsername = serverConfig.basicAuthUsername.value,
|
||||
basicAuthPassword = serverConfig.basicAuthPassword.value,
|
||||
authMode = serverConfig.authMode.value,
|
||||
authUsername = serverConfig.authUsername.value,
|
||||
authPassword = serverConfig.authPassword.value,
|
||||
basicAuthEnabled = false,
|
||||
basicAuthUsername = null,
|
||||
basicAuthPassword = null,
|
||||
// misc
|
||||
debugLogsEnabled = serverConfig.debugLogsEnabled.value,
|
||||
gqlDebugLogsEnabled = false, // deprecated
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.graphql.mutations.SettingsMutation
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.graphql.types.toStatus
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
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.MangaTable
|
||||
import suwayomi.tachidesk.server.database.dbTransaction
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.io.InputStream
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
@@ -525,7 +527,15 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.graphql.types.Settings
|
||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
@@ -45,9 +46,13 @@ data class BackupServerSettings(
|
||||
@ProtoNumber(27) override var globalUpdateInterval: Double,
|
||||
@ProtoNumber(28) override var updateMangas: Boolean,
|
||||
// Authentication
|
||||
@ProtoNumber(29) override var basicAuthEnabled: Boolean,
|
||||
@ProtoNumber(30) override var basicAuthUsername: String,
|
||||
@ProtoNumber(31) override var basicAuthPassword: String,
|
||||
@ProtoNumber(56) override var authMode: AuthMode,
|
||||
@ProtoNumber(29) override var basicAuthEnabled: Boolean?,
|
||||
@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
|
||||
@ProtoNumber(32) override var debugLogsEnabled: Boolean,
|
||||
@ProtoNumber(33) override var gqlDebugLogsEnabled: Boolean,
|
||||
|
||||
@@ -11,6 +11,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.http.HandlerType
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.RedirectResponse
|
||||
import io.javalin.http.UnauthorizedResponse
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -22,14 +24,19 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import suwayomi.tachidesk.global.GlobalAPI
|
||||
import suwayomi.tachidesk.graphql.GraphQL
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.manga.MangaAPI
|
||||
import suwayomi.tachidesk.opds.OpdsAPI
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
import suwayomi.tachidesk.server.util.Browser
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
object JavalinSetup {
|
||||
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 ->
|
||||
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 requiresAuthentication = !isPreFlight && !isWebManifest
|
||||
@@ -120,14 +168,31 @@ object JavalinSetup {
|
||||
return@beforeMatched
|
||||
}
|
||||
|
||||
val authMode = serverConfig.authMode.value ?: AuthMode.NONE
|
||||
|
||||
fun credentialsValid(): Boolean {
|
||||
val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false
|
||||
val (username, password) = basicAuthCredentials
|
||||
return username == serverConfig.basicAuthUsername.value &&
|
||||
password == serverConfig.basicAuthPassword.value
|
||||
return username == serverConfig.authUsername.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")
|
||||
throw UnauthorizedResponse()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
||||
@@ -89,6 +90,42 @@ class ServerConfig(
|
||||
.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 port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
|
||||
@@ -120,14 +157,6 @@ class ServerConfig(
|
||||
// extensions
|
||||
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
|
||||
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
|
||||
@@ -139,9 +168,20 @@ class ServerConfig(
|
||||
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
|
||||
// Authentication
|
||||
val basicAuthEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val basicAuthUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val basicAuthPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue(EnumConfigAdapter(AuthMode::class.java))
|
||||
val authUsername: 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
|
||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
|
||||
@@ -9,7 +9,12 @@ package suwayomi.tachidesk.server
|
||||
|
||||
import android.os.Looper
|
||||
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.ConfigValue
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.parser.ConfigDocument
|
||||
import dev.datlag.kcef.KCEF
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.createAppModule
|
||||
@@ -32,12 +37,14 @@ import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import suwayomi.tachidesk.global.impl.KcefWebView.Companion.toCefCookie
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.manga.impl.update.Updater
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
||||
import suwayomi.tachidesk.server.BooleanConfigAdapter
|
||||
import suwayomi.tachidesk.server.database.databaseUp
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
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 =
|
||||
module {
|
||||
single { applicationDirs }
|
||||
@@ -268,7 +298,40 @@ fun applicationSetup() {
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
logger.error(e) { "Exception while creating initial server.conf" }
|
||||
|
||||
@@ -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
|
||||
|
||||
# Authentication
|
||||
server.basicAuthEnabled = false
|
||||
server.basicAuthUsername = ""
|
||||
server.basicAuthPassword = ""
|
||||
server.authMode = "none" # none, basic_auth or simple_login
|
||||
server.authUsername = ""
|
||||
server.authPassword = ""
|
||||
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
|
||||
164
server/src/main/resources/static/login.html
Normal file
164
server/src/main/resources/static/login.html
Normal 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>
|
||||
@@ -15,6 +15,9 @@
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "Roboto","Helvetica","Arial",sans-serif;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0em;
|
||||
}
|
||||
body.disconnected::after {
|
||||
content: 'Disconnected, please refresh';
|
||||
@@ -30,7 +33,8 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
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;
|
||||
padding: 8px 32px;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
# Authentication
|
||||
server.basicAuthEnabled = false
|
||||
server.basicAuthUsername = ""
|
||||
server.basicAuthPassword = ""
|
||||
server.authMode = "none" # none, basic_auth or simple_login
|
||||
server.authUsername = ""
|
||||
server.authPassword = ""
|
||||
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
|
||||
Reference in New Issue
Block a user