Basic JWT implementation (#1524)

* Basic JWT implementation

* Move JWT to UI_LOGIN mode and bring back SIMPLE_LOGIN as before

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

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

* Refresh: Update only access token

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

* Implement JWT Audience

* Store JWT key

Generates the key on startup if not set

* Handle invalid Base64

* Make JWT expiry configurable

* Missing value parse

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

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

* Simplify Duration parsing

* JWT Protect Mutations

* JWT Protect Queries and Subscriptions

* JWT Protect v1 WebSockets

* WebSockets allow sending token via protocol header

* Also respect the `suwayomi-server-token` cookie

* JWT reduce default token expiry

* JWT Support cookie on WebSocket as well

* Lint

* Authenticate graphql subscription via connection_init payload

* WebView: Prefer explicit token over cookie

This hack was implemented because WebView sent `"null"` if no token was
supplied, just don't send a bad token, then we can do this properly

* WebView: Implement basic login dialog if no token supplied

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-08-21 00:04:48 +02:00
committed by GitHub
parent d90bfb6e3e
commit 8547159eec
60 changed files with 1567 additions and 410 deletions

View File

@@ -6,12 +6,14 @@ import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.FilterChange
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.Preference
@@ -25,7 +27,10 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
class SourceMutation {
@@ -39,7 +44,11 @@ class SourceMutation {
val meta: SourceMetaType,
)
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
fun setSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetSourceMetaInput,
): DataFetcherResult<SetSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input
return asDataFetcherResult {
@@ -61,7 +70,11 @@ class SourceMutation {
val source: SourceType?,
)
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
fun deleteSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteSourceMetaInput,
): DataFetcherResult<DeleteSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, key) = input
return asDataFetcherResult {
@@ -116,7 +129,11 @@ class SourceMutation {
val hasNextPage: Boolean,
)
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
fun fetchSourceManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchSourceMangaInput,
): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, type, page, query, filters) = input
return future {
@@ -182,7 +199,11 @@ class SourceMutation {
val source: SourceType,
)
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
fun updateSourcePreference(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateSourcePreferenceInput,
): DataFetcherResult<UpdateSourcePreferencePayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, change) = input
return asDataFetcherResult {