[#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
* - 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) }
}
}

View File

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

View File

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

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?
// 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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 {
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;
}

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